Protocol Buffers 一种轻便高效的结构化数据存储格式

介绍

Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

安装

安装过程中,参考了Google Protocol Buffer 的使用和原理Protobuf

了解到了下面几点:

  1. 该项目的 php 语言版本,可通过纯 php 包和本地 c 扩展来运行启动;
  2. 纯 php 包是为了提供在 php 平台上更广的可用性,而 c 扩展更注重性能;
  3. 两者的实现,提供了同一套 APIs ,应该是由同一套代码生成的;
  4. 如果用户想在他们两者之间切换,不需要重新修改/生成代码;
  5. 两者都实现了通过生成 php 代码来实现在 php 中的 message 和 enum 类型;
  6. 作者建议通过 php 来生成,支持 .proto 文件;
  7. 这个包的构建过程仅在当前目录安装扩展/包,其次还要安装该协议的编译包去最终完成 php 生成功能。

体会

了解之后,感觉实现起来还是比较麻烦,毕竟很多项目其实已经用了很久了,rpc 接口众多,一下子迁移起来其实是比较困难的。如果是新建的项目倒是可以尝试,但是每次都需要预先定义这些数据结构,其实也是带来一定的难度,因为数据结构可能会不断的变化。理论上 google 的这个协议是让不同的语言不必去例会数据结构,而只需要关注具体的数据。但是代码毕竟还是人来写的,其实用起来反而不如 json 这些标准化跨平台包含数据结构的格式。

感觉暂时还不适合在大项目中使用,毕竟开发不仅仅是一个人,而是一个团队。

如何反爬虫及恶意采集

前言

恶意采集、攻击及非搜索引擎爬虫,影响了服务器的网络,同时也有可能暴露一些存在风险/SQL注入的网站。

我们虽然是识别出了,但是他们的抓取还是没有停止,有时候一天就是上百万次,严重影响了服务器网络状况,只有千日做贼,没有千日防贼的道理。

因此,从社会工程学的角度,不仅仅要防范,还要主动出击,增加难度。

主要策略

  1. 加入延时,让他采集难度增加,效率下降,如每天100万采集,把响应时间从0.1s延长到5s,那么就可以减少98%的访问,同样的时间内,恶意爬虫一天仅能采集2万;
  2. 适当返回正确状态的假数据、垃圾内容、扰乱他们正常采集,让他们花大量时间在正确内容的甄别上;
  3. 通过跳转的形式,把爬虫的攻击引回他自己的服务器、竞争对手的网站;
  4. 通过跳转的形式,让爬虫去下载大容量 iso 文件(国内各大镜像),搞挂他的内存、数据库;
  5. 返回一些包含恶意js、非标准的html内容,搞坏他们的爬虫分析,及时中止对方程序;
  6. 必要时,可以使用杀手锏,跳转到当地公安局、境外安全组织、黑客组织。

TLS 1.3:改进延迟,部分握手加密

制定进展

IETF正在制定TLS协议的最新版本1.3,截至目前还没有正式发布,新的 TLS 1.3 基于 TLS 1.2,将移除对部分弱加密算法如 MD5 和 SHA-22 的支持。

TLSv1.3 新特性

  • 将不再支持静态RSA密钥交换,握手将默认使用前向安全(Perfect Forward Secrecy),一个安全性足够强的 Diffie-Hellman 参数。
  • 将移除 ChangeCipherSpec协议。
  • 将大幅改进握手,注重隐私,在握手尽可能早的阶段加密信息,ServerHello 之后的所有消息都是加密的,客户端只需要一次往返就能与服务器建立安全和验证的连接。
  • 客户端将在 ClientHello 阶段将提高 key_share(共用密钥)。
  • Sessions 将在握手完成之后,才会被建立。
  • Renegotiation攻击不可能发生。
  • 绝大部分的握手将处于加密状态。
  • 很多类型的信息现在也有了扩展(如 Certificate Transparency)
    改进后的握手流程

支持程度

  • nginx
    在 nginx 的最新 mainline 版本 1.13.0 中,可以看到有一条这样的介绍
    1
    Feature: the "TLSv1.3" parameter of the "ssl_protocols" directive.

也就是nginx在最新的版本已经加入这个配置选项,并且支持1.3版本的 TLS 了。

  • Firefox
    Firefox 52 已正式支持 TLS 1.3。
  • Chrome Dev
    Google Chrome 浏览器已经在测试版本中开始支持TLS 1.3了。

速度提升

在升级 HTTPS 的过程中,很多大型网站会有所顾虑,因为相比传统 HTTP 协议,TLS 的握手会耗费更多的延时,进而会影响网站的打开时间,并且使用TLS还是有可能在第一次连接的时候受到中间人攻击。
在 TLS 1.2 时代,我们可以通过完美前向加密、HTTP/2、HSTS、HSTS preload list、CHACHA20_POLY1305 协议等方式,提升网站的安全性,加快网站的访问速度,这方面也确实起到了很好的效果。

降低延时

访问安全加密网站时,首先要建立共用密钥,这一过程叫做一次握手。有专门的加密消息往来于浏览器和网站之间,每一次连接,TLS握手就随之发生。
通过移动网络访问时,由于 4G 网络的延时,加载时间可能会增加上百ms不等,也会影响到用户体验。

两代协议握手对比
对于 TLS 1.2,每次请求需要2次消息往来,才能完成握手。
对于 TLS 1.3,首次握手只需要1次消息往来,可以节约几十到上百ms不等。

TLS 1.3 改进增加了“不额外增加网络延时”模式(0-RTT)。对于近期访问过的站点,可以直接发送有用的数据,而不需要经过握手。
针对0-RTT及其他相关的扩展性,早在2016年5月之前,微信就基于TLS1.3草案标准,设计实现了一套安全通信协议mmtls。基于TLS1.3的微信安全通信协议mmtls介绍

因此,加上 HTTP/2 对多资源加载的优化,通过 HTTPS + HTTP/2 完全有希望使得速度比传统 HTTP 协议更快更安全。

Chrome 开启TLSv1.3

在地址栏输入chrome://flags/

  • 找到Maximum TLS version enabled.并选择TLS 1.3
  • 重启浏览器

OpenSSL支持

近期OpenSSL也发布了一篇新文章Using TLS1.3 With OpenSSL
该文章介绍了新版本的开发进展及一些需要注意事项。
OpenSSL 1.1.1 版本即将发布,该版本将支持 TLSv1.3,将完全兼容于原有1.1.0,理论上,只要应用支持 1.1.0,那么可以直接升级到 1.1.1。将自动支持 TLSv1.3,而不需要做任何其他操作。
编译的时候,需增加选项enable-tls1_3

新增加密套件

  • TLS13-AES-256-GCM-SHA384
  • TLS13-CHACHA20-POLY1305-SHA256
  • TLS13-AES-128-GCM-SHA256
  • TLS13-AES-128-CCM-8-SHA256
  • TLS13-AES-128-CCM-SHA256

nginx推荐配置

nginx 配置

这本来是个很简单的配置,但是还是有很多开发者没留意、没注意、对 TLS 加密等等不了解,会有一些错误的配置。
而网上的配置也参差不齐,经常有很多错误的安全套件顺序,在网上排名特别高,这里还是推荐一下Mozilla的配置,有3个档次,大家可以按自己的用户分布情况,选择对应合适的配置。
Mozilla 的推荐配置

推荐配置

这个配置能够最低兼容到 Firefox 1, Chrome 1, IE 7, Opera 5 以及 Safari 1。

  • Ciphersuites: ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
  • Versions: TLSv1.2, TLSv1.1, TLSv1
  • TLS curves: prime256v1, secp384r1, secp521r1
  • Certificate type: RSA
  • Certificate curve: 'None
  • Certificate signature: sha256WithRSAEncryption
  • RSA key size: 2048
  • DH Parameter size: 2048
  • ECDH Parameter size: 256
  • HSTS: max-age=15768000
  • Certificate switching: None

Linux文件系统修复

前因

当前的操作系统是openSUSE,系统目录使用的是btrfs文件系统,用户目录使用的是xfs文件系统。由于公司突然间断电,导致系统完全崩溃,开机后直接进入grub修复界面。初步判断是因为,计算机使用的固态硬盘,加上断电时,计算机还在做关键操作,操作完成之后,写文件的校验值checksum和文件不一致。最终导致系统目录无法读取。

处理过程

用户目录挂载盘符为/dev/sdb4
恢复时仅需一行代码:

1
xfs_repair /dev/sdb4 -L

系统目录挂载盘符为/dev/sdb3
恢复时我并没有恢复成功,直接放弃了,反正我也用不到。
但通过查看帮助似乎有下面这种方法:

1
btrfsck --repair --init-csum-tree --init-extent-tree  <device>

但我执行后不成功,于是我直接保留用户目录,然后重新安装了。

缓存的伪随机及定期清理

缓存的伪随机策略

有时候在业务逻辑上,我们需要返回一些随机的数据,越随机越好,这时候缓存较短的时间、不缓存等方式均不理想。缓存一定的时间会导致用户会以为页面没刷新,没取到数据,体验不好。
而若不缓存,大量的请求可能会造成数据库、中间件的巨大压力。
可以通过设置特定的键值+随机数的方式,来人为造成一定数量随机出来的缓存,完美解决随机问题。

1
2
3
4
5
6
7
8
$key = 'key:sample:'.rand(1, 500);
$value = $redis->get($key);
if (!$value) {
$value = self::getData('...');
if ($key) {
$redis->setex($key, rand(600, 1200), $value);
}
}

缓存时间随机+key键值随机,可以产生500种(甚至更多)组合,满足线上的需求。

长期缓存,定期清理

当我们做了缓存之后,有时候缓存的时间不能设置太长,因为总会有一些数据在变更。变更后,如果很长的时间内,缓存一直有效,那么修改的操作等于没有成功。
缓存的时间短,也导致了缓存命中率不高,内存利用率较小,反而增加了多余的两次缓存读写(缓存取不到数据,从数据库/中间件获取,再次写入缓存),性能提升不够明显。
其实对比热数据而言,99%的在库数据,是长时间不会变化的,可以通过检测数据库插入/变更/删除时间,通过特定的调度脚本,及时从缓存种移除。
如果做了这些工作,那么缓存的设置就可以大胆的设置为长期缓存,半年,整月等策略,缓存的命中率能提升一个大台阶。

缓存置换策略的选择

在redis中,当缓存的内存占用量达到峰值maxmemory的时候,这时候就需要用到置换策略了,这个策略不设置或设置错误,都会导致浪费,甚至影响系统正常运行,抛出错误,502网关错误等等。redis一共支持6种策略。

  • noeviction: 不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
  • allkeys-lru: 优先删除掉最近最不经常使用的key,用以保存新数据
  • volatile-lru: 只从设置失效(expire set)的key中选择最近最不经常使用的key进行删除,用以保存新数据
  • allkeys-random: 随机从all-keys中选择一些key进行删除,用以保存新数据
  • volatile-random: 只从设置失效(expire set)的key中,选择一些key进行删除,用以保存新数据
  • volatile-ttl: 只从设置失效(expire set)的key中,选出存活时间(TTL)最短的key进行删除,用以保存新数据

缓存与基础业务系统分离

如果基础业务系统与缓存放在同一redis库,那么设置了volatile-lru,就会导致正常用户可能被迫下线。设置了volatile-ttl就会导致一些缓存时间较短的缓存,没有起到作用,频繁被擦除,影响命中率,缓存效果也不理想。
若缓存与基础业务系统分离,既可以优化缓存的命中率等考核条件。同时也可以保证用户正常的使用不受影响,如session、用户登录状态、邮箱/手机验证码等数据。

session

在使用session时,应选择按照volatile-ttl策略,并发量高时,删除长期不在线的用户。

缓存

在缓存方面,通过volatile-lru,近期最少使用算法,讲命中率低的缓存及时删除,把空间让给更需要的缓存。

开启TCP BBR算法

开启BBR

开启之前,需先升级至4.9内核,开启bbr算法,vim /etc/sysctl.conf

1
2
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr

执行,使其保存生效

1
sysctl -p

检查是否开启

检查内核是否已开启bbr

1
2
sysctl net.ipv4.tcp_available_congestion_control
lsmod | grep bbr

openSUSE升级Linux内核版本

可以通过官方的一个网站package查找到所需要的包,选择新的版本或者当
前版本,选择对应的版本下载。

安装Linux 4.9

1
rpm -ivh kernel-default-4.9.0-2.1.x86_64.rpm

迁移Redis过程中存在的问题总结

需求的产生

由于公司业务需求,我对较高访问量的页面做了全局随机缓存优化,主要缓存了部分API、RPC接口。特点是代码层无需变化,无需改变原有逻辑,有较高的可控性和可维护性,降低了代码的冗余。同时对缓存做了进一步的封装,便于下一步的维护与变更。
增加随机缓存主要是防止缓存数据穿透问题,就是确保缓存数据不会同时在一个特定的时间全部失效,进而导致RPC、数据库请求剧增。
同时提供了打印请求过程、记录参数、超时告警、请求时间、超时自动结束等功能。便于进一步跟踪代码的执行情况。

产生的问题

由于原先的Memcached使用的是代理机制,1带4的方案,缓存的一天请求数可能接近1亿次左右,老的Memcached可能没有考虑到大规模应用缓存的需求,在高峰时期,有可能会挂掉。
甚至由于Memcached所在的服务器单一IP,Memcached的请求数在某一时刻会达到好几万,巨大的请求数没有及时结束,造成了大量的Time Wait,已经影响到正常的PHP服务。

期间,我们也做过一些尝试,看能否更换机器解决,最终发现还是远远无法满足未来的需求,急需扩容。

Redis的选择

公司原有众多业务也有使用到Redis、Kafka,考虑到Redis支持的类型更多,在扩容的时候,直接切换到Redis似乎是更好的选择。

Redis Cluster

一开始我们定了这个方案,一是他的高可用,二是它能够做到自动扩容,可以把机器当成资源池使用。同时配置了多台主从,理论上应该是性能超佳才是。
但在测试阶段就遇到众多问题,发现在单台服务器连接的时候,就出现了请求数过大、网卡过载、连接超时等问题。主要原因可能是:
1、长连接及连接池,PHP的机制导致了PHP是没有内存这个说法,没有办法建立一个长连接,然后让所有进程来共享使用,同时由于默认自带的参数是使用的长连接的形式,会导致进程结束,但是连接并未结束,一直占用着端口。在使用PHPRedis发现无法解决存在的问题,我还尝试了使用PRedis,发现问题还是一样,巨大的连接数对服务器是一个很大的负担,通过修改系统配置也没有很好的解决。
2、网卡可能不够用了,Redis Cluster的机制我没有特别深入去了解,也不是很明白为什么几千的请求数就能够把千兆网卡打满,这是否说明了Redis Cluster不仅仅把数据放大了2倍,而是n倍?同时还需要以转发的形式,来取得真正的值?这与Redis支持10万请求/秒的理论值相距深远。
3、Redis Cluster可能还不够成熟,大规模应用是否可行、是否经过完全测试?文档不齐全、案例较少、没有一个很好的PHP客户端也是一个大问题。

其中有一个同事提出能否由PHP端来分片,使用短连接,直接写入Redis Cluster,最终我们发现,似乎原有方法不支持这个功能,连接单机时会提示错误。

Redis

最终由于时间有限,我们暂时是选择了Redis单机的模式,经过对Linux系统的配置,降低了实时的请求数占用情况。同时在代码层实现分片功能。

1
2
3
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

升级OpenSSL支持ALPN

升级OpenSSL

我使用的Linux系统是CentOS7,但是OpenSSL的版本其实还是很低,并不能够及时支持google chrome的新版本。而HTTP/2还是需要服务端的ssl提供支持,这是一个前提,我选择了openssl-1.1.0b。
首先要下载,解压,编译。

1
2
3
4
5
6
7
8
wget https://www.openssl.org/source/openssl-1.1.0b.tar.gz
tar zxf openssl-1.1.0b.tar.gz
cd openssl-1.1.0b
./config --prefix=/usr --shared
make
make depend
make test
make install

先查看当前版本情况,做好备份。

1
2
openssl version
mv /usr/bin/openssl /root/

替换,查看版本,重启nginx即可,若不行,可能还需要重新安装nginx。

1
2
3
ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl
openssl version
lnmp nginx reload

使用swoole搭建socket长连接

Swoole简介

通常基于php扩展使用纯PHP就可以完全实现异步网络服务器和客户端程序。但是想实现一个类似于多IO线程,还是有很多繁琐的编程工作要做,包括如何来管理连接,如何来保证数据的收发原则性,网络协议的处理。
开源项目Swoole使用C语言和PHP结合来完成了这项工作。灵活多变的业务模块使用PHP开发效率高,基础的底层和协议处理部分用C语言实现,保证了高性能。它以扩展的方式加载到了PHP中,提供了一个完整的网络通信的框架,然后PHP的代码去写一些业务。它的模型是基于多线程Reactor+多进程Worker,既支持全异步,也支持半异步半同步。

Swoole的特点

  • Accept线程,解决Accept性能瓶颈和惊群问题
  • 多IO线程,可以更好地利用多核
  • 提供了全异步和半同步半异步2种模式
  • 处理高并发IO的部分用异步模式
  • 复杂的业务逻辑部分用同步模式
  • 底层支持了遍历所有连接、互发数据、自动合并拆分数据包、数据发送原子性。

为何选择Swoole及Swoole性能

包括腾讯企业QQ、聚美优品等众多公司的大规模部署和实践。
我在网站挂载的服务器上做了测试,由于服务器配置可能不高,仅能支持2万并发。具体性能还有待调整提高。

简单部署

选择了8000端口作为socket连接端口,其中server.php代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
//创建websocket服务器对象,监听0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 8000);

//监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
var_dump($request->fd, $request->get, $request->server);
$ws->push($request->fd, "hello, welcome\n");
});

//监听WebSocket消息事件
$ws->on('message', function ($ws, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data},this is yatesun");
});

//监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
echo "client-{$fd} is closed\n";
});

$ws->start();

执行php server.php,启动swoole服务,监听8000端口,可以在系统中查看到端口已经监听成功,接下来可以通过chrome中的Console,执行js代码,来测试服务是否正常运行。

JS调试

1
2
3
4
5
6
7
var exampleSocket = new WebSocket("ws://119.29.182.88:8000");
exampleSocket.onopen = function (event) {
exampleSocket.send("亲爱的服务器!我连上你啦!");
};
exampleSocket.onmessage = function (event) {
console.log(event.data);
};

接下来执行下面代码,则能收到服务器返回的信息:

1
exampleSocket.send("爱你哦");

server: 爱你哦,this is yatesun