Apr
28
想要计算一个区域里对角线的和,但SUMIF里面的那个criteria实在太简陋了,只能用vba来实现,大概长这样:
然后这么用:
引用
Function sum_diag(n As Integer, ParamArray args() As Variant) As Variant
result = 0
For i = LBound(args) To UBound(args)
For Each elem In args(i)
If elem.Row + elem.Column = n Then
result = result + elem.Value
End If
Next elem
Next i
sum_diag = result
End Function
result = 0
For i = LBound(args) To UBound(args)
For Each elem In args(i)
If elem.Row + elem.Column = n Then
result = result + elem.Value
End If
Next elem
Next i
sum_diag = result
End Function
然后这么用:
引用
=sum_diag(ROW()+COLUMN(), $B$1:$D$3)
Apr
23
两年前学会了用Google Authenticator(详见为SSH添加两步验证),远程服务就装上了,感觉放心了很多。
但当时只是简单加上了Google Authenticator,实际使用中既要输入验证又要输入密码,太繁琐了,所以在搭建我司跳板机的时候,选择了用 publickey + authenticator 的方案,只需要输入一次验证码即可。
具体的配置方案变化不大,主要是用上了 SSH 6.2+ 新增的 AuthenticationMethods 参数,可以指定一系列验证方法,具体配置如下:
顺便吐槽一下,Linux这套东西折腾起来真是要命,今天配置跳板机备份机的时候,完全相同的配置,复制一份就是不对,虽然配置里只指定了publickey,keyboard-interactive,但是每次输完验证码以后还是要求输入密码才行,折腾了几个小时才发现,不知道从什么时候开始,"auth required pam_google_authenticator.so" 已经不合适了,需要改成 "auth sufficient pam_google_authenticator.so",这样才会在输入验证码以后就结束认证过程(sufficient的实现里加了一个break?什么鬼。)(感谢 @ https://serverfault.com/a/740881/343388)
以及,上篇也提到过的,另外一个坑是 ubuntu/debian 下面在自己编译完pam模块以后,需要手动拷贝到 /lib/security/ 目录下面才行,唉……
但当时只是简单加上了Google Authenticator,实际使用中既要输入验证又要输入密码,太繁琐了,所以在搭建我司跳板机的时候,选择了用 publickey + authenticator 的方案,只需要输入一次验证码即可。
具体的配置方案变化不大,主要是用上了 SSH 6.2+ 新增的 AuthenticationMethods 参数,可以指定一系列验证方法,具体配置如下:
引用
#默认需要先用publickey验证,再用验证码
AuthenticationMethods publickey,keyboard-interactive
#对于指定的IP,只需要publickey验证
Match Address 10.0.0.4
AuthenticationMethods publickey
#也可以指定用户只需要publickey验证
#Match User XXX
#AuthenticationMethods publickey
AuthenticationMethods publickey,keyboard-interactive
#对于指定的IP,只需要publickey验证
Match Address 10.0.0.4
AuthenticationMethods publickey
#也可以指定用户只需要publickey验证
#Match User XXX
#AuthenticationMethods publickey
顺便吐槽一下,Linux这套东西折腾起来真是要命,今天配置跳板机备份机的时候,完全相同的配置,复制一份就是不对,虽然配置里只指定了publickey,keyboard-interactive,但是每次输完验证码以后还是要求输入密码才行,折腾了几个小时才发现,不知道从什么时候开始,"auth required pam_google_authenticator.so" 已经不合适了,需要改成 "auth sufficient pam_google_authenticator.so",这样才会在输入验证码以后就结束认证过程(sufficient的实现里加了一个break?什么鬼。)(感谢 @ https://serverfault.com/a/740881/343388)
以及,上篇也提到过的,另外一个坑是 ubuntu/debian 下面在自己编译完pam模块以后,需要手动拷贝到 /lib/security/ 目录下面才行,唉……
Apr
22
在我司的运维实践中,sudo承担了一个很边缘,但是却很有意思的任务
最简单应用是,在web服务器上,在配置完nginx和php的log以后需要重启service,就这么玩(修改/etc/sudoers):
如此一来,nginx用户可以执行 sudo service nginx reload 或者 sudo nginx configtest,也可以执行 sudo php5-fpm reload,但不能执行 sudo php5-fpm stop
不过被玩出花来的还是我司的跳板机。
对于管理员,我们这样配置:
通过密码验证切换到root用户
对于组长,我们这样配置:
$ sudo groupadd master #添加组
$ sudo usermod -a -G master felix021 #将用户添加到这个组
$ vi /etc/sudoers
这个 getpubkey 是一个shell脚本,只包含一句 cat "/home/$user/.ssh/id_rsa.pub" ,用来获取某个用户的公钥
然后配合 authroize_user 这个脚本,将公钥发送到组长有权限访问的机器上,通过这种方式实现一个简陋的二级授权:
另外顺便提一下sudo的debug,似乎相关资料很少。在配置group的时候,直接去测试,有时会发现好像总是不对,但是sudo默认没有log,很难排查问题。实际上只要在 /etc/ 下面添加一个 sudo.conf 就好,文件内容为:
再次执行sudo的时候就会看到debug log文件里的信息:
从这里可以看到,虽然前面把 felix021 添加到了 master 这个group下,但是sudo并没有识别出来。
放狗搜了一下才知道,原来linux下需要退出所有的登录session重新登录,才能生效。
最简单应用是,在web服务器上,在配置完nginx和php的log以后需要重启service,就这么玩(修改/etc/sudoers):
引用
nginx ALL= NOPASSWD: /sbin/service nginx *, /sbin/service php5-fpm reload
如此一来,nginx用户可以执行 sudo service nginx reload 或者 sudo nginx configtest,也可以执行 sudo php5-fpm reload,但不能执行 sudo php5-fpm stop
不过被玩出花来的还是我司的跳板机。
对于管理员,我们这样配置:
引用
felix021 ALL=(ALL:ALL) ALL
通过密码验证切换到root用户
对于组长,我们这样配置:
$ sudo groupadd master #添加组
$ sudo usermod -a -G master felix021 #将用户添加到这个组
$ vi /etc/sudoers
引用
%master ALL= NOPASSWD: /usr/bin/getpubkey *
这个 getpubkey 是一个shell脚本,只包含一句 cat "/home/$user/.ssh/id_rsa.pub" ,用来获取某个用户的公钥
然后配合 authroize_user 这个脚本,将公钥发送到组长有权限访问的机器上,通过这种方式实现一个简陋的二级授权:
引用
sudo getpubkey $1 | ssh nginx@$host bash -c "cat /dev/stdin >> ~/.ssh/authorized_keys"
另外顺便提一下sudo的debug,似乎相关资料很少。在配置group的时候,直接去测试,有时会发现好像总是不对,但是sudo默认没有log,很难排查问题。实际上只要在 /etc/ 下面添加一个 sudo.conf 就好,文件内容为:
引用
Debug sudo /var/log/sudo_debug all@warn,plugin@info
再次执行sudo的时候就会看到debug log文件里的信息:
引用
Apr 22 14:12:03 sudo[21786] user_info: user=felix021
Apr 22 14:12:03 sudo[21786] user_info: pid=21786
Apr 22 14:12:03 sudo[21786] user_info: ppid=21785
Apr 22 14:12:03 sudo[21786] user_info: pgid=21785
Apr 22 14:12:03 sudo[21786] user_info: tcpgid=21785
Apr 22 14:12:03 sudo[21786] user_info: sid=21522
Apr 22 14:12:03 sudo[21786] user_info: uid=1002
Apr 22 14:12:03 sudo[21786] user_info: euid=0
Apr 22 14:12:03 sudo[21786] user_info: gid=1000
Apr 22 14:12:03 sudo[21786] user_info: egid=1000
Apr 22 14:12:03 sudo[21786] user_info: groups=1000
Apr 22 14:12:03 sudo[21786] user_info: pid=21786
Apr 22 14:12:03 sudo[21786] user_info: ppid=21785
Apr 22 14:12:03 sudo[21786] user_info: pgid=21785
Apr 22 14:12:03 sudo[21786] user_info: tcpgid=21785
Apr 22 14:12:03 sudo[21786] user_info: sid=21522
Apr 22 14:12:03 sudo[21786] user_info: uid=1002
Apr 22 14:12:03 sudo[21786] user_info: euid=0
Apr 22 14:12:03 sudo[21786] user_info: gid=1000
Apr 22 14:12:03 sudo[21786] user_info: egid=1000
Apr 22 14:12:03 sudo[21786] user_info: groups=1000
从这里可以看到,虽然前面把 felix021 添加到了 master 这个group下,但是sudo并没有识别出来。
放狗搜了一下才知道,原来linux下需要退出所有的登录session重新登录,才能生效。
Nov
23
最近发现有些服务器会定期出现磁盘过载问题,这里记录一下追查过程,供参考。
11月10日,立山向我反映我们线上的某 service 出现了一小段时间的无响应,查看 error log,发现有几百条"208203204 connect() to unix:/var/run/php5-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream"错误,期间 zabbix 报警 server disk io overloaded,这让我想起确实每隔 3 ~ 4 天 zabbix 都会上报 server disk io overloaded(但出现的时间点并不固定在早上或晚上,也不一定是钱牛牛的访问高峰期),与 service 的error log时间也吻合,由于该 server 也是我们钱牛牛的两台 web 服务之一,因此在磁盘过载期间,钱牛牛对外提供的服务也收到了一定影响(error log也能证实这一点)。
用于 zabbix 的监控报警和 error log 的信息都太少,无法判断发生原因,因此没有继续追查下去;但是13日早晨这个问题又出现,因此决定重视起来。我在 server 上安装了 iotop 这个工具,使用 crontab 每分钟执行:
每隔 1s 记录一次当前访问磁盘的进程及访问速度等信息,记录 5 次后退出。
在17号捕捉到又一次磁盘过载,通过 iotop 的输出:
可以看到除了知道是 php-fpm 进程在写磁盘之外,并没有什么卵用,但至少还是指明了方向,只要找出 php 在写什么文件,就能离发现原因更近。
因此我写了另一个 monitor.py (后附),实时监控 iotop 的输出,筛选出磁盘 io 过大的进程,找出这些进程打开的文件(ls -lh /proc/$PID/fd),上报到sentry:
又等了5天,今天(22号)终于抓到罪魁祸首:
server: PID(4252) IS USING TOO MUCH DISK IO
{
"iotop": "07:44:17 4252 be/4 nginx 288.76 K/s 94813.59 K/s 0.00 % 75.15 % php-fpm: pool www",
"proc": "/proc/4252/fd:
total 0
lrwx------ 1 nginx users 64 Nov 22 07:44 0 -> socket:[1286391831]
lrwx------ 1 nginx users 64 Nov 22 07:44 1 -> /dev/null
lrwx------ 1 nginx users 64 Nov 22 07:44 2 -> /dev/null
lrwx------ 1 nginx users 64 Nov 22 07:44 3 -> socket:[2138228391]
lrwx------ 1 nginx users 64 Nov 22 07:44 4 -> socket:[2138229146]
l-wx------ 1 nginx users 64 Nov 22 07:44 5 -> /data/www/xxx-service/runtime/logs/app.log
lr-x------ 1 nginx users 64 Nov 22 07:44 6 -> /data/www/xxx-service/runtime/logs/app.log
",
"time": "2016-11-21 07:44:17"
}
从这里可以看出,php-fpm是在读写 service 的log。log文件内容有点琐碎,但是跟往常比起来确实没有什么异常,但是文件本身有点异常:
可以看出,所有的log文件都是在磁盘负载特别高的时候修改的,可见,磁盘负载高的直接原因是 yii 框架的 logrotate 机制导致的。
以下是从 yii2/framework2/vendor/yiisoft/yii2/log/FileTarget.php 拷贝出来的内容:
可以看出,罪魁祸首是 $rotateByCopy 默认值是 true ,而 yii2 之所以这么做,(根据框架的注释)是因为在 windows 下 log文件 很可能正被另一个文件打开,导致 rename 失败(吐槽:难道就不能多写一行代码根据检测到的os的type设置这个值吗???)。这也解释了为什么每个被 rotate 的 log 文件的修改时间间隔1分钟。
既然找到了问题的原因,解决方案就很简单了,把这个属性修改为false即可,当然,更完善的方案是能够根据OS的类型自动检测这个值。根据这个思路,我向 yii2 官方提交了一个pull requests:https://github.com/yiisoft/yii2/pull/13057,希望能被 merge 进去吧。
完。
monitor.py:
11月10日,立山向我反映我们线上的某 service 出现了一小段时间的无响应,查看 error log,发现有几百条"208203204 connect() to unix:/var/run/php5-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream"错误,期间 zabbix 报警 server disk io overloaded,这让我想起确实每隔 3 ~ 4 天 zabbix 都会上报 server disk io overloaded(但出现的时间点并不固定在早上或晚上,也不一定是钱牛牛的访问高峰期),与 service 的error log时间也吻合,由于该 server 也是我们钱牛牛的两台 web 服务之一,因此在磁盘过载期间,钱牛牛对外提供的服务也收到了一定影响(error log也能证实这一点)。
用于 zabbix 的监控报警和 error log 的信息都太少,无法判断发生原因,因此没有继续追查下去;但是13日早晨这个问题又出现,因此决定重视起来。我在 server 上安装了 iotop 这个工具,使用 crontab 每分钟执行:
引用
$ /usr/sbin/iotop -btoqqqk --iter=5
每隔 1s 记录一次当前访问磁盘的进程及访问速度等信息,记录 5 次后退出。
在17号捕捉到又一次磁盘过载,通过 iotop 的输出:
引用
19:09:05 9663 be/4 nginx 31583.10 K/s 31551.68 K/s 0.00 % 93.86 % php-fpm: pool www
可以看到除了知道是 php-fpm 进程在写磁盘之外,并没有什么卵用,但至少还是指明了方向,只要找出 php 在写什么文件,就能离发现原因更近。
因此我写了另一个 monitor.py (后附),实时监控 iotop 的输出,筛选出磁盘 io 过大的进程,找出这些进程打开的文件(ls -lh /proc/$PID/fd),上报到sentry:
引用
$ /usr/sbin/iotop -btoqqqk | ./monitor.py
又等了5天,今天(22号)终于抓到罪魁祸首:
server: PID(4252) IS USING TOO MUCH DISK IO
{
"iotop": "07:44:17 4252 be/4 nginx 288.76 K/s 94813.59 K/s 0.00 % 75.15 % php-fpm: pool www",
"proc": "/proc/4252/fd:
total 0
lrwx------ 1 nginx users 64 Nov 22 07:44 0 -> socket:[1286391831]
lrwx------ 1 nginx users 64 Nov 22 07:44 1 -> /dev/null
lrwx------ 1 nginx users 64 Nov 22 07:44 2 -> /dev/null
lrwx------ 1 nginx users 64 Nov 22 07:44 3 -> socket:[2138228391]
lrwx------ 1 nginx users 64 Nov 22 07:44 4 -> socket:[2138229146]
l-wx------ 1 nginx users 64 Nov 22 07:44 5 -> /data/www/xxx-service/runtime/logs/app.log
lr-x------ 1 nginx users 64 Nov 22 07:44 6 -> /data/www/xxx-service/runtime/logs/app.log
",
"time": "2016-11-21 07:44:17"
}
从这里可以看出,php-fpm是在读写 service 的log。log文件内容有点琐碎,但是跟往常比起来确实没有什么异常,但是文件本身有点异常:
引用
nginx@server:logs$ ls -lah
total 5.0G
drwxrwxrwx 2 nginx users 4.0K Nov 22 10:54 .
drwxrwxrwx 3 nginx users 4.0K Jul 28 15:15 ..
-rwxrwxrwx 1 nginx users 55M Nov 22 12:04 app.log
-rw-r--r-- 1 nginx users 1001M Nov 22 07:44 app.log.1
-rw-r--r-- 1 nginx users 1001M Nov 22 07:43 app.log.2
-rw-r--r-- 1 nginx users 1001M Nov 22 07:42 app.log.3
-rw-r--r-- 1 nginx users 1001M Nov 22 07:41 app.log.4
-rw-r--r-- 1 nginx users 1001M Nov 22 07:40 app.log.5
total 5.0G
drwxrwxrwx 2 nginx users 4.0K Nov 22 10:54 .
drwxrwxrwx 3 nginx users 4.0K Jul 28 15:15 ..
-rwxrwxrwx 1 nginx users 55M Nov 22 12:04 app.log
-rw-r--r-- 1 nginx users 1001M Nov 22 07:44 app.log.1
-rw-r--r-- 1 nginx users 1001M Nov 22 07:43 app.log.2
-rw-r--r-- 1 nginx users 1001M Nov 22 07:42 app.log.3
-rw-r--r-- 1 nginx users 1001M Nov 22 07:41 app.log.4
-rw-r--r-- 1 nginx users 1001M Nov 22 07:40 app.log.5
可以看出,所有的log文件都是在磁盘负载特别高的时候修改的,可见,磁盘负载高的直接原因是 yii 框架的 logrotate 机制导致的。
以下是从 yii2/framework2/vendor/yiisoft/yii2/log/FileTarget.php 拷贝出来的内容:
public $rotateByCopy = true;
...
protected function rotateFiles()
{
$file = $this->logFile;
for ($i = $this->maxLogFiles; $i >= 0; --$i) {
// $i == 0 is the original log file
$rotateFile = $file . ($i === 0 ? '' : '.' . $i);
if (is_file($rotateFile)) {
// suppress errors because it's possible multiple processes enter into this section
if ($i === $this->maxLogFiles) {
@unlink($rotateFile);
} else {
if ($this->rotateByCopy) {
@copy($rotateFile, $file . '.' . ($i + 1));
if ($fp = @fopen($rotateFile, 'a')) {
@ftruncate($fp, 0);
@fclose($fp);
}
} else {
@rename($rotateFile, $file . '.' . ($i + 1));
}
}
}
}
}
...
protected function rotateFiles()
{
$file = $this->logFile;
for ($i = $this->maxLogFiles; $i >= 0; --$i) {
// $i == 0 is the original log file
$rotateFile = $file . ($i === 0 ? '' : '.' . $i);
if (is_file($rotateFile)) {
// suppress errors because it's possible multiple processes enter into this section
if ($i === $this->maxLogFiles) {
@unlink($rotateFile);
} else {
if ($this->rotateByCopy) {
@copy($rotateFile, $file . '.' . ($i + 1));
if ($fp = @fopen($rotateFile, 'a')) {
@ftruncate($fp, 0);
@fclose($fp);
}
} else {
@rename($rotateFile, $file . '.' . ($i + 1));
}
}
}
}
}
可以看出,罪魁祸首是 $rotateByCopy 默认值是 true ,而 yii2 之所以这么做,(根据框架的注释)是因为在 windows 下 log文件 很可能正被另一个文件打开,导致 rename 失败(吐槽:难道就不能多写一行代码根据检测到的os的type设置这个值吗???)。这也解释了为什么每个被 rotate 的 log 文件的修改时间间隔1分钟。
既然找到了问题的原因,解决方案就很简单了,把这个属性修改为false即可,当然,更完善的方案是能够根据OS的类型自动检测这个值。根据这个思路,我向 yii2 官方提交了一个pull requests:https://github.com/yiisoft/yii2/pull/13057,希望能被 merge 进去吧。
完。
monitor.py:
#!/usr/bin/python
#coding:utf-8
import sys
import re
import time
import datetime
import socket
try:
import simplejson as json
except:
import json
import subprocess
from raven import Client
import requests
last_sent = 0
dsn = '__SENTRY_DSN__'
#00 - '19:07:03'
#01 - '9663'
#02 - 'be/4'
#03 - 'nginx'
#04 - '10423.06'
#05 - 'K/s'
#06 - '10423.06'
#07 - 'K/s'
#08 - '0.00'
#09 - '%'
#10 - '99.99'
#11 - '%'
#12 - 'php-fpm: pool www'
def should_skip(program):
if program == '[kjournald]':
return True
for prefix in ['gzip', 'rsync', 'ssh', 'logrotate', 'sap100', 'sar ', 'rpm ', 'updatedb', 'mysql', 'nginx', 'vim', 'cat']:
if program.startswith(prefix):
return True
return False
def run_command(*cmd):
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
if err:
raise Exception(err.strip())
return out
except Exception, e:
return 'run %s failed: %s' % (str(cmd), e)
while True:
try:
line = sys.stdin.readline().strip()
except KeyboardInterrupt:
print >>sys.stderr, "user abort"
sys.exit(0)
fields = re.split(' +', line.strip(), 12)
if len(fields) != 13:
continue
if should_skip(fields[12]):
continue
read_speed = float(fields[4])
write_speed = float(fields[6])
if read_speed > 1000 or write_speed > 1000:
date = time.strftime('%Y-%m-%d')
pid = fields[1]
client = Client(dsn)
message = '%s: PID(%s) IS USING TOO MUCH DISK IO' % (socket.gethostname(), pid)
args = {
'time' : date + ' ' + fields[0],
'iotop' : line.strip(),
'proc' : run_command('ls', '-lhR', '/proc/%s/fd' % pid),
}
print >>sys.stderr, message
print >>sys.stderr, json.dumps(args, indent=4)
client.capture('raven.events.Message', message=message, extra=args)
#coding:utf-8
import sys
import re
import time
import datetime
import socket
try:
import simplejson as json
except:
import json
import subprocess
from raven import Client
import requests
last_sent = 0
dsn = '__SENTRY_DSN__'
#00 - '19:07:03'
#01 - '9663'
#02 - 'be/4'
#03 - 'nginx'
#04 - '10423.06'
#05 - 'K/s'
#06 - '10423.06'
#07 - 'K/s'
#08 - '0.00'
#09 - '%'
#10 - '99.99'
#11 - '%'
#12 - 'php-fpm: pool www'
def should_skip(program):
if program == '[kjournald]':
return True
for prefix in ['gzip', 'rsync', 'ssh', 'logrotate', 'sap100', 'sar ', 'rpm ', 'updatedb', 'mysql', 'nginx', 'vim', 'cat']:
if program.startswith(prefix):
return True
return False
def run_command(*cmd):
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
if err:
raise Exception(err.strip())
return out
except Exception, e:
return 'run %s failed: %s' % (str(cmd), e)
while True:
try:
line = sys.stdin.readline().strip()
except KeyboardInterrupt:
print >>sys.stderr, "user abort"
sys.exit(0)
fields = re.split(' +', line.strip(), 12)
if len(fields) != 13:
continue
if should_skip(fields[12]):
continue
read_speed = float(fields[4])
write_speed = float(fields[6])
if read_speed > 1000 or write_speed > 1000:
date = time.strftime('%Y-%m-%d')
pid = fields[1]
client = Client(dsn)
message = '%s: PID(%s) IS USING TOO MUCH DISK IO' % (socket.gethostname(), pid)
args = {
'time' : date + ' ' + fields[0],
'iotop' : line.strip(),
'proc' : run_command('ls', '-lhR', '/proc/%s/fd' % pid),
}
print >>sys.stderr, message
print >>sys.stderr, json.dumps(args, indent=4)
client.capture('raven.events.Message', message=message, extra=args)
Nov
16
与某供应商对接的时候,要求用他们的RSA公钥加密,抛过来一个 RSAUtil.java ,核心代码大概是这样的:
RSA什么的,用php来做不要太简单,顺手就能写出来:
结果果然不行,发过去以后,对方表示无法解密,而我没有对方的私钥,不好验证,没办法,只能自己动手,造一对rsa密钥:
试了下,确实和 java 的结果不互通。不过在测试过程中发现一个现象:java生成的加密串总是一样的,而php生成的加密串总是不一样的。google搜了一下"php openssl_public_encrypt different everytime", Stack Overflow 的解释是,PHP的 openssl_public_encrypt 默认使用 PKCS#1 算法,引入随机数,用于防止流量探测(频率分析、密文匹配什么的,我就不懂了)。
所以很显然,Bouncy Castle 没有使用 PKCS#1 算法,放狗搜到官方文档说,Cipher.getInstance("RSA", "BC") ,第一个参数 RSA 相当于 "RSA/NONE/NoPadding" (当然也可以指定 RSA/NONE/PKCS1Padding )。
看了下 php的openssl_public_encrypt文档,可以给第四个参数“padding”指定不同的值,例如 OPENSSL_NO_PADDING ,但是试了下,发现直接失败了,只好再放狗,竟然搜到了 php的bugreport,还好第一个回复就说明了原因:需要手动用 ASCII 0 填充到 blocksize 才行(当然rsa并不禁止使用其他value,主要是加解密双方要约定好)。
验证了一下,用 OPENSSL_NO_PADDING 能够正常解密 java 生成的密文,并且在明文前面填充了若干 ASCII 0 ,补全到128字节,就此解决问题:
public byte[] encrypt(byte[] data, PublicKey pk) throws Exception {
Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
cipher.init(Cipher.ENCRYPT_MODE, pk);
byte[] raw = new byte[128]; //cipher.getBlockSize() = 128
cipher.doFinal(data, 0, data.length, raw, 0);
return raw;
}
Cipher cipher = Cipher.getInstance("RSA", new org.bouncycastle.jce.provider.BouncyCastleProvider());
cipher.init(Cipher.ENCRYPT_MODE, pk);
byte[] raw = new byte[128]; //cipher.getBlockSize() = 128
cipher.doFinal(data, 0, data.length, raw, 0);
return raw;
}
RSA什么的,用php来做不要太简单,顺手就能写出来:
function rsa_encrypt($plain_text, $public_key_path)
{
$public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
openssl_public_encrypt($plain_text, $encrypted, $public_key);
return bin2hex($encrypted);
}
function rsa_decrypt($encrypted, $private_key_path)
{
$private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key);
return $plain_text;
}
{
$public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
openssl_public_encrypt($plain_text, $encrypted, $public_key);
return bin2hex($encrypted);
}
function rsa_decrypt($encrypted, $private_key_path)
{
$private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key);
return $plain_text;
}
结果果然不行,发过去以后,对方表示无法解密,而我没有对方的私钥,不好验证,没办法,只能自己动手,造一对rsa密钥:
引用
$ openssl genrsa -out test.pem 1024
$ openssl rsa -in test.pem -pubout -out test_public.pem
$ openssl rsa -in test.pem -pubout -out test_public.pem
试了下,确实和 java 的结果不互通。不过在测试过程中发现一个现象:java生成的加密串总是一样的,而php生成的加密串总是不一样的。google搜了一下"php openssl_public_encrypt different everytime", Stack Overflow 的解释是,PHP的 openssl_public_encrypt 默认使用 PKCS#1 算法,引入随机数,用于防止流量探测(频率分析、密文匹配什么的,我就不懂了)。
所以很显然,Bouncy Castle 没有使用 PKCS#1 算法,放狗搜到官方文档说,Cipher.getInstance("RSA", "BC") ,第一个参数 RSA 相当于 "RSA/NONE/NoPadding" (当然也可以指定 RSA/NONE/PKCS1Padding )。
看了下 php的openssl_public_encrypt文档,可以给第四个参数“padding”指定不同的值,例如 OPENSSL_NO_PADDING ,但是试了下,发现直接失败了,只好再放狗,竟然搜到了 php的bugreport,还好第一个回复就说明了原因:需要手动用 ASCII 0 填充到 blocksize 才行(当然rsa并不禁止使用其他value,主要是加解密双方要约定好)。
验证了一下,用 OPENSSL_NO_PADDING 能够正常解密 java 生成的密文,并且在明文前面填充了若干 ASCII 0 ,补全到128字节,就此解决问题:
function rsa_encrypt($plain_text, $public_key_path)
{
$public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
openssl_public_encrypt(str_pad($plain_text, 128, "\0", STR_PAD_LEFT), $encrypted, $public_key, OPENSSL_NO_PADDING);
return bin2hex($encrypted);
}
function rsa_decrypt($encrypted, $private_key_path)
{
$private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key, OPENSSL_NO_PADDING);
return ltrim($plain_text, "\0");
}
{
$public_key = openssl_pkey_get_public(file_get_contents($public_key_path));
openssl_public_encrypt(str_pad($plain_text, 128, "\0", STR_PAD_LEFT), $encrypted, $public_key, OPENSSL_NO_PADDING);
return bin2hex($encrypted);
}
function rsa_decrypt($encrypted, $private_key_path)
{
$private_key = openssl_pkey_get_private(file_get_contents($private_key_path));
openssl_private_decrypt(hex2bin($encrypted), $plain_text, $private_key, OPENSSL_NO_PADDING);
return ltrim($plain_text, "\0");
}
Sep
21
虽然我们使用的腾讯云自带了一份监控,但是实在太弱,不好用(上次因为crontab导致postfix的maildrop把inode用光,腾讯云没有任何报警信息),所以我们额外搭建了一套Zabbix用于线上服务的监控,用来发现更细粒度的系统问题,例如io过载、cpu过载、内存不足、磁盘空间不足、服务down了之类的事件。
发现了事件需要及时报警,但是zabbix自带的那个邮件通知太弱(不支持需要用户名和密码的smtp服务器),所以需要自定义通知。zabbix文档在有对自定义通知的说明,但是太粗略了,折腾了好久终于搞明白,这里记录一下备查。
1. 编写通知发送脚本
参照Custom alertscripts,写一个脚本,接受三个命令行参数 "to subject body" (即收件人、标题、正文),把消息发送出去。例如我们的短信通知是这样的(注: message.test.com是我们内部的统一通知服务):
文件名: sms
除此之外我们还配置了通过 email、sentry 报警的脚本。
2. 将通知脚本置于 zabbix_server.conf 配置的 AlertScriptsPath 目录下
注意:必须在这个目录下、zabbix进程有执行权限
3. 在zabbix控制台配置通知脚本
4. 配置用户支持的通知方法
4. 配置触发器
5. 触发事件测试效果
根据情况启动若干个耗cpu的进程,例如:
$ cat /dev/urandom | md5sum
发现了事件需要及时报警,但是zabbix自带的那个邮件通知太弱(不支持需要用户名和密码的smtp服务器),所以需要自定义通知。zabbix文档在有对自定义通知的说明,但是太粗略了,折腾了好久终于搞明白,这里记录一下备查。
1. 编写通知发送脚本
参照Custom alertscripts,写一个脚本,接受三个命令行参数 "to subject body" (即收件人、标题、正文),把消息发送出去。例如我们的短信通知是这样的(注: message.test.com是我们内部的统一通知服务):
文件名: sms
引用
#!/usr/bin/python
import sys
import requests
to = sys.argv[1]
subject = sys.argv[2]
body = sys.argv[3]
message = '%s: %s' % (subject, body)
print requests.post('http://message.test.com/sms/send', {
'request_source' : 'monitor',
'mobile' : to,
'message' : message
})
import sys
import requests
to = sys.argv[1]
subject = sys.argv[2]
body = sys.argv[3]
message = '%s: %s' % (subject, body)
print requests.post('http://message.test.com/sms/send', {
'request_source' : 'monitor',
'mobile' : to,
'message' : message
})
除此之外我们还配置了通过 email、sentry 报警的脚本。
2. 将通知脚本置于 zabbix_server.conf 配置的 AlertScriptsPath 目录下
注意:必须在这个目录下、zabbix进程有执行权限
3. 在zabbix控制台配置通知脚本
引用
Administration -> Media Types -> (右上角) Create Media Type
Name: sms
Type: Script
Script name: sms (这里写脚本的名字就行,不用全路径)
Enabled: 打勾
Save.
Name: sms
Type: Script
Script name: sms (这里写脚本的名字就行,不用全路径)
Enabled: 打勾
Save.
4. 配置用户支持的通知方法
引用
Administration -> Users -> 点击需要配置的用户(例如Admin) -> Media -> Add
Type: sms
Send to: 13800138000 (这就是脚本接受到第一个参数)
When Active: 1-7, 00:00-24:00 (7*24,运维的悲剧)
Use if severity: 根据情况勾选
Status: Enabled
Add.
Save. (别漏了点这里!)
Type: sms
Send to: 13800138000 (这就是脚本接受到第一个参数)
When Active: 1-7, 00:00-24:00 (7*24,运维的悲剧)
Use if severity: 根据情况勾选
Status: Enabled
Add.
Save. (别漏了点这里!)
4. 配置触发器
引用
Configuration -> Actions -> (右上角) create action
#Action
Name: send to sms
#Operations (以下是参考选择)
a) 点击New
b) 添加收件人:Send to Users 右边的 Add 选择 Admin
c) 选择通知方式:Send only to 选择 sms
d) 点击Add
e) 点击Save
Add和Save都得点,否则就又2了
#Action
Name: send to sms
#Operations (以下是参考选择)
a) 点击New
b) 添加收件人:Send to Users 右边的 Add 选择 Admin
c) 选择通知方式:Send only to 选择 sms
d) 点击Add
e) 点击Save
Add和Save都得点,否则就又2了
5. 触发事件测试效果
根据情况启动若干个耗cpu的进程,例如:
$ cat /dev/urandom | md5sum
Aug
14
记得很早之前就想给自己的电脑启用BitLocker,但是因为主板上没有TPM芯片(用于存储密钥),不能给系统盘加密;而不能给系统盘加密的话,其他盘的解密就需要每次启动系统以后再输入一次密钥,然而我的文档、桌面等一般都存在其他盘,我估摸着会出现一些奇怪的现象,所以就放弃了。
昨天兴起又研究了一下,发现其实并不是一定要有TPM才能给系统盘加密——这个限制是Vista第一次引入BitLocker时的要求,后来微软也意识到,由于大多数民用主板上没有集成TPM芯片,导致BitLocker略显鸡肋,于是在Win7开始做了个变通,允许把加密密钥保存在单独的启动分区里,通过一个密码(或启动U盘)来保证密钥的安全,但是需要在组策略编辑器里将“计算机配置-管理模板-Windows组件-BitLocker驱动器加密-操作系统驱动器-启动时需要附加身份验证”修改为已启用(注意左下角“没有兼容的TPM时允许BitLocker”被打上勾了),系统盘被加密后,用户在启动电脑时输入一次密码即可。
对Win7系统安装略有了解的同学大概注意到了,从光盘启动安装的话,除了Windows系统盘之外,还会创建一个100M的启动分区和一个恢复分区,其中启动分区一方面是为了兼容UEFI,另一方面也一定程度上解决了TPM的问题。虽然可以不需要TPM了,但是必须要说一句,这样的安全性还是降低了,因为启动分区是不加密的,这意味着有心人可以替换启动器(启动分区的第一个扇区,或者BOOTMGR)收集到用户启动时输入的密码,从而获得加密密钥。我想这也是微软默认仍然需要tpm,除非用户主动修改组策略的原因吧。
我以前一直很不喜欢Windows的这个小动作(凭什么一个系统要占据3个Primary Partition?要知道MBR方式的磁盘只支持4个主分区或者3主分区+1逻辑分区,这么搞真浪费),所以我通常是用Wim安装器来安装,只需要一个主分区,结果就把自己坑了……还好补救措施很简单,通过DiskPark或者DiskGenius或者diskmgmt.msc开一个100M的主分区(注意不能是逻辑分区),激活(设置为启动分区,此处应有55AA梗),格式化为NTFS,把系统盘的 boot目录、bootmgr文件 拷贝到新分区,再用管理员cmd执行 "bcdedit /export X:\boot\bcd" ,重启电脑后就能开启BitLocker了。
开启了BitLocker以后感觉心里踏实了很多,毕竟Chrome保存了那么多密码,如果电脑被偷走还是挺头疼的。
昨天兴起又研究了一下,发现其实并不是一定要有TPM才能给系统盘加密——这个限制是Vista第一次引入BitLocker时的要求,后来微软也意识到,由于大多数民用主板上没有集成TPM芯片,导致BitLocker略显鸡肋,于是在Win7开始做了个变通,允许把加密密钥保存在单独的启动分区里,通过一个密码(或启动U盘)来保证密钥的安全,但是需要在组策略编辑器里将“计算机配置-管理模板-Windows组件-BitLocker驱动器加密-操作系统驱动器-启动时需要附加身份验证”修改为已启用(注意左下角“没有兼容的TPM时允许BitLocker”被打上勾了),系统盘被加密后,用户在启动电脑时输入一次密码即可。
对Win7系统安装略有了解的同学大概注意到了,从光盘启动安装的话,除了Windows系统盘之外,还会创建一个100M的启动分区和一个恢复分区,其中启动分区一方面是为了兼容UEFI,另一方面也一定程度上解决了TPM的问题。虽然可以不需要TPM了,但是必须要说一句,这样的安全性还是降低了,因为启动分区是不加密的,这意味着有心人可以替换启动器(启动分区的第一个扇区,或者BOOTMGR)收集到用户启动时输入的密码,从而获得加密密钥。我想这也是微软默认仍然需要tpm,除非用户主动修改组策略的原因吧。
我以前一直很不喜欢Windows的这个小动作(凭什么一个系统要占据3个Primary Partition?要知道MBR方式的磁盘只支持4个主分区或者3主分区+1逻辑分区,这么搞真浪费),所以我通常是用Wim安装器来安装,只需要一个主分区,结果就把自己坑了……还好补救措施很简单,通过DiskPark或者DiskGenius或者diskmgmt.msc开一个100M的主分区(注意不能是逻辑分区),激活(设置为启动分区,此处应有55AA梗),格式化为NTFS,把系统盘的 boot目录、bootmgr文件 拷贝到新分区,再用管理员cmd执行 "bcdedit /export X:\boot\bcd" ,重启电脑后就能开启BitLocker了。
开启了BitLocker以后感觉心里踏实了很多,毕竟Chrome保存了那么多密码,如果电脑被偷走还是挺头疼的。
Jul
28
上篇对比了阿里云、腾讯云、UCloud的云数据库,没想到文章被 Linux.cn 推荐,并被今日头条收录,影响范围颇大,相关云数据库团队都有工程师跟我取得了联系,尤其是阿里云和腾讯云的同学表示内部自测的性能并不差,向我要了具体的测试参数做核对;此外Linux中国的微信号下也有好奇宝宝回复表示想知道具体的测试命令和参数等。因此抽了点时间写个续篇,补充一些上篇没有提到的内容。
腾讯云的同学在测试完以后反馈,TPS 达到了 750 左右,与上篇中的结果相差悬殊。收到邮件,我感到深深的忧虑,难道之前的测试有问题?由于阿里云的同学说有些客户反馈性能差是因为测试的主机离数据库比较远,于是我重新申请了和上次配置一致的云数据库和云主机(并且特意check了所在分区是否一致),再次执行测试,发现在腾讯云上达到了 733 TPS,确实远超之前的测试结果。稳妥起见,我在阿里云和UCloud也申请了新的主机和数据库,再跑了一遍测试,结果如下:
两次的结果并不是很稳定(证明了上次的测试确实有点粗糙,至少应该测几次取均值),然而在腾讯云上测试的两次结果相差过于悬殊,远不是误差能够解释的。所以我仔细研究了一下各家云数据库的说明,发现了一点玄机:
如上图所示,针对测试选择的实例,腾讯云和阿里云明确说明了实例的标称QPS,而UCloud则没有明确说明。从使用方的角度,我认为这个标称值是实例能保证的QPS,测试结果也间接证明了这一点。
可以看出,因为使用的是标准化测试工具,TPS和QPS是之间有着1:27的比例关系。参考腾讯云标称的 7200 QPS,那么第一次测试的 7500 QPS / 277.8 TPS 就可以解释得通了:可能是该实例所在的物理机上负载较高所以执行了严格的限制,也可能是腾讯云不同机器上部署的频控策略不同(AB测试?),总之不巧正好测到了限制比较严格的实例。
从表中还可以看出一些其他比较有意思的地方:
1. 阿里云之所以表现得垫底,是因为QPS限制在4000,虽然第一次测试达到了5700,但仍然低于另外两家。如果能够采用最高档次的实例(14000 QPS),那么至少也能达到 500+ TPS,并不差。
2. 实测QPS最高的是UCloud,远高于另外两家同档次的实例;他们在文档中给出了一组测试结果,号称最高能达到5.8万QPS。但是UCloud给自己留了余地,没有标称值,没有做出承诺,显得不够严谨。此外,我觉得,不能基于低QPS就武断地认为技术实力弱(想想双十一的量,阿里都扛得住),可能只是具体的运营或开发策略不同。
两次测试和进一步的分析更完整地体现了三家云数据库的状况。但正如上篇所说,性能往往并不是唯一/最高标准:即使选择了限制最严格的阿里云,在千万级的数据量下仍然能达到 200+ TPS,已经远超大部分业务的实际需求了。
具体的测试脚本我放到了 Gist 上,供参考,如有不妥之处,欢迎指教探讨:
复测
腾讯云的同学在测试完以后反馈,TPS 达到了 750 左右,与上篇中的结果相差悬殊。收到邮件,我感到深深的忧虑,难道之前的测试有问题?由于阿里云的同学说有些客户反馈性能差是因为测试的主机离数据库比较远,于是我重新申请了和上次配置一致的云数据库和云主机(并且特意check了所在分区是否一致),再次执行测试,发现在腾讯云上达到了 733 TPS,确实远超之前的测试结果。稳妥起见,我在阿里云和UCloud也申请了新的主机和数据库,再跑了一遍测试,结果如下:
分析
两次的结果并不是很稳定(证明了上次的测试确实有点粗糙,至少应该测几次取均值),然而在腾讯云上测试的两次结果相差过于悬殊,远不是误差能够解释的。所以我仔细研究了一下各家云数据库的说明,发现了一点玄机:
如上图所示,针对测试选择的实例,腾讯云和阿里云明确说明了实例的标称QPS,而UCloud则没有明确说明。从使用方的角度,我认为这个标称值是实例能保证的QPS,测试结果也间接证明了这一点。
可以看出,因为使用的是标准化测试工具,TPS和QPS是之间有着1:27的比例关系。参考腾讯云标称的 7200 QPS,那么第一次测试的 7500 QPS / 277.8 TPS 就可以解释得通了:可能是该实例所在的物理机上负载较高所以执行了严格的限制,也可能是腾讯云不同机器上部署的频控策略不同(AB测试?),总之不巧正好测到了限制比较严格的实例。
从表中还可以看出一些其他比较有意思的地方:
1. 阿里云之所以表现得垫底,是因为QPS限制在4000,虽然第一次测试达到了5700,但仍然低于另外两家。如果能够采用最高档次的实例(14000 QPS),那么至少也能达到 500+ TPS,并不差。
2. 实测QPS最高的是UCloud,远高于另外两家同档次的实例;他们在文档中给出了一组测试结果,号称最高能达到5.8万QPS。但是UCloud给自己留了余地,没有标称值,没有做出承诺,显得不够严谨。此外,我觉得,不能基于低QPS就武断地认为技术实力弱(想想双十一的量,阿里都扛得住),可能只是具体的运营或开发策略不同。
总结
两次测试和进一步的分析更完整地体现了三家云数据库的状况。但正如上篇所说,性能往往并不是唯一/最高标准:即使选择了限制最严格的阿里云,在千万级的数据量下仍然能达到 200+ TPS,已经远超大部分业务的实际需求了。
彩蛋
具体的测试脚本我放到了 Gist 上,供参考,如有不妥之处,欢迎指教探讨:
#!/bin/bash
#注:很多发行版自带SysBench的是0.4.12,但有些参数(如oltp-tables-count)是0.5才支持的,建议从官方源获取源码编译运行
#注2:ubuntu 16.04 上面编译如果报错说没有 libmysqlclient_r.so ,自己建一个软链接就好了
#注3:需要先create database mysql, 并对用于测试的用户授予 sbtest 这个库的读写权限。
cmd="sysbench --test=/root/src/sysbench-0.5/sysbench/tests/db/oltp.lua \
--oltp-table-size=2000000 \
--oltp-tables-count=20 \
--oltp-range-size=100 \
--oltp-point-selects=10 \
--oltp-simple-ranges=6 \
--oltp-sum-ranges=2 \
--oltp-order-ranges=3 \
--oltp-distinct-ranges=2 \
--oltp-index-updates=1 \
--oltp-non-index-updates=1 \
--num-threads=32
--max-requests=0
--max-time=1800
--report-interval=10
--rand-init=on
--rand-type=special
--rand-spec-iter=12
--rand-spec-pct=10
--rand-spec-res=75
--verbosity=3
--mysql-host=10.0.0.1
--mysql-port=3306
--mysql-user=root
--mysql-password=123456
"
$cmd cleanup
$cmd prepare
$cmd run
#注:很多发行版自带SysBench的是0.4.12,但有些参数(如oltp-tables-count)是0.5才支持的,建议从官方源获取源码编译运行
#注2:ubuntu 16.04 上面编译如果报错说没有 libmysqlclient_r.so ,自己建一个软链接就好了
#注3:需要先create database mysql, 并对用于测试的用户授予 sbtest 这个库的读写权限。
cmd="sysbench --test=/root/src/sysbench-0.5/sysbench/tests/db/oltp.lua \
--oltp-table-size=2000000 \
--oltp-tables-count=20 \
--oltp-range-size=100 \
--oltp-point-selects=10 \
--oltp-simple-ranges=6 \
--oltp-sum-ranges=2 \
--oltp-order-ranges=3 \
--oltp-distinct-ranges=2 \
--oltp-index-updates=1 \
--oltp-non-index-updates=1 \
--num-threads=32
--max-requests=0
--max-time=1800
--report-interval=10
--rand-init=on
--rand-type=special
--rand-spec-iter=12
--rand-spec-pct=10
--rand-spec-res=75
--verbosity=3
--mysql-host=10.0.0.1
--mysql-port=3306
--mysql-user=root
--mysql-password=123456
"
$cmd cleanup
$cmd prepare
$cmd run