微信号:fangtalk

介绍:马上入行10年了,为了不想一直贴个程序猿的标签,从码代码、做架构,转向练习码字.我还是一个刚刚开始写字的孩子,认真专注写字的孩子.都说7年可以精通一个行业,IT不敢说精通但也算入行;希望再一个7年以后,我...

实战明星投票活动四天三夜(技术宅 PK 脑残粉)

2016-01-27 08:50 Anthon Liu

写在正文开始前,先要感谢大房和另一位尚不知名的某高手对我上一篇讲CLOSE_WAIT里的一点错误提出的疑问,之前文章里最后我提到了由于有CLOSE_WAIT的连接导致后面新的连接无法创建。 这句说法是有问题的,因为要达到新的连接都无法创建的地步的话,CLOSE_WAIT起码要几万个以上 或者CPU100%(Socket FD 到上线,而我的截图里都没有体现)。 所以正确的解答应该是,由于我们使用Tornado单进程模式,而且有128个backlog限制,当使用urllib2 调用第三方接口时很慢,导致大量的连接堆积出现等待。而后面新连接的请求当排队等到被处理时,SLB upstream的连接已经超时了。所以从客户端的角度来看,你的请求就像是被挂起了,没有响应。


再次感谢大房,并且有机会我想请那位细心的技术宅喝杯咖啡,因为这代表这篇文章被仔细看了。让人欣慰!


这本是我司的一次年末市场活动营销活动,通过我们的自有APP,微信公众号,微博,朋友圈等社交手段发起一个年度最热最火明星投票活动。基于HTML5的活动页面,真的,真的就只有3张(这还要算上一张静态规则页面)。说实话,当初做项目评估的时候双方都没太当回事,直到经历了提心吊胆的四天三夜后才发现当初想简单了。


DAY 1


木有啥特别的,开发团队收到线框图,视觉设计然后开始码代码(今天不是来讲解细节技术的,所以到底是用ReactJS还是YUI,又或者是用bootstrap还是其他的就不做展开了).  唯一要特别感谢的就是这次运营的主办部门负责具体操作的哥们,他不断的在我耳边重复:”小心啊,一定要防范下刷票啊,很凶残的啊“。因此我和开发团队之间也做过了讨论,并在有限的条件下也加了一些防范的措施。


现在回想起来作为一个技术人员,特别是在互联网公司的开发人员,除了技术之外你要懂活动运营到底是干嘛的(下周会单独写一篇运营的文章说说自己的一些陋见)。尤其尤其要明白运营的对象以及他们的特征,因为,他们是你的用户或者是你的对手!


当天晚上,我们接到了电话,预期会有比较大的动作来推广这次投票要做号准备。技术这边的第一反映当然就是并发可能比较高,因此我们当天晚上做了一次调整



  • 从自建的反向代理替换为云SLB(负载均衡)

  • 使用高性能的OpenResty 嵌入 Lua API做请求处理

  • 直接使用Redis做数据的缓存

  • 主要图片全部走CDN

  • 基于缓存做基于IP的请求数限制


这些做法的主要权衡考量为:

    1. 不要单点失效!

    阿里云的SLB自带健康监控,流量控制,端口映射,且最重要的是它不是一台前端反向代理,它没有单点失效(Single Point Failure)。

    2. 速度! 速度!

    相比基于Nginx + FastCGI的 PHP 或者Nginx + WSGI的 Python 还要转发请求的模式来说,直接内嵌成Nginx Bundle的nginx_lua显然更快,而且基于寄存器的Lua语言特性本身就比基于堆栈的PHP与Python要好上不少。

     3. 分流! 分流!

    你钱再多,单台服务器的吞吐总量是固定的,且这里还受到带宽,网络地域等多方因素的影响。 如何能将网站的流量引导多个 域名下的多个站点(比如所有图片是一个站,所有JS + CSS是一个,主页面是量一个)。这样非但可以减轻自己服务器对处理静态资源的带宽及CPU压力,也可以很大程度提高客户端加载速度(不懂我在说啥的,请务必看下雅虎网页性能优化34黄金守则)


DAY 2


市场部门开始推广了,粉丝开始展露獠牙,支撑服务器的压力上来了。微博评论里开始大量反馈刷不出页面,看不见投票列表,点击投票没反映,吓死宝宝了。上服务器看了一圈,发现我们使用的一个第三方投票统计服务的接口正大批量的超时。赶紧的,联系对方的技术,一问才知道他们有流量限制(心中顿时1万个个%¥&%#$, 我们是付费用户啊... 而且随着请求增加你还要扣钱)。和他们市场周旋了一番总算同意暂时先帮我们提高上限。好了没多久又不行了,和市场同事沟通后果断决定,停止第三方API调用。这个时候权衡的思路为:


有损服务(可以参考某厂技术架构变迁文章):

当你的量达到你当前装载量的上限而扩容不可能立即完成时,可以选择有损服务。有损即暂停或者减少某些非核心功能使得更多的计算资源可以用在核心业务上。


如果没有第三方服务,最差就是我们缺失了漂亮的投票活动报表。相比现在不能用导致影响整个活动来说,这是一个很容易作的决定。


DAY 3


熬过推广第一天本以为差不多也就是这个量了,谁知道TFboys 和 EXO 的粉再一次推翻了我们对他们热情的预估,一早开始服务就爆了。


好吧,我给跪了,继续看瓶颈在哪里。这次轮到Redis了,也是该到它了(毕竟上图就可以看到这个是一个热点)。检查负载的老四样:

  • CPU

  • Memory

  • IO

  • Net


CPU接近 70%, 内存还可以。 IO 峰值有点高但是IO wait很低。 网络就比较糟糕了,平均:

  • 2000多个ESTABLISHED

  • 4000多个TIME_WAIT

  • 几百个CLOSE_WAIT


这里推荐一个自己常用的shell 命令:


while true;do netstat -atnp | awk '/^tcp/ {++S[$6]} END {for(a in S) print a, S[a]}';sleep 5;echo --------------------;done


ESTABLISHED可以理解,毕竟并发那么多呢。4000多个TIME_WAIT虽然在这个数量上不会导致太多问题,但处女座的我看着就不舒服。要搞掉。顺便也自己也偷偷吐吐舌头,这台服务器从建立开始貌似的确没有优化过。


vi /etc/sysctl.conf

net.ipv6.conf.all.disable_ipv6 =1

net.ipv4.conf.all.arp_announce=2

net.ipv4.conf.lo.arp_announce=2

fs.file-max=65535

net.core.netdev_max_backlog=30000

net.core.somaxconn=10000

net.core.rps_sock_flow_entries=32768

net.ipv4.tcp_max_syn_backlog=10000

net.ipv4.tcp_max_tw_buckets=10000

net.ipv4.tcp_fin_timeout=10

net.ipv4.tcp_timestamps=1

net.ipv4.tcp_tw_recycle=0

net.ipv4.tcp_tw_reuse=0

net.ipv4.tcp_synack_retries=2

net.ipv4.tcp_window_scaling=1

net.ipv4.tcp_keepalive_time=180

net.ipv4.tcp_keepalive_intvl=30

net.ipv4.tcp_keepalive_probes=5

net.core.wmem_default=8388608

net.core.rmem_default=8388608

net.core.rmem_max=16777216

net.core.wmem_max=16777216

net.ipv4.tcp_syncookies=1

net.ipv4.tcp_mem=94500000915000000 927000000

net.ipv4.tcp_max_orphans=3276800

net.ipv4.ip_local_port_range=102465535

(感谢大房的赞助,省得我去敲了)


其中黄色的几条比较重要的,分别做了下面这些:


  • 关闭IPV6, 目前IPV6 支持的真不多而且会引起域名查询延迟,在网络连接中不必要地尝试连接到IPv6地址导致延迟。所以不要装b,老老实实关了

  • 增加打开文件最大数量限制,这个和ulimit -n做的事情是一样的但是这个是系统级别的,ulimit是进程级别的

  • 增加2个backlog,这2个都是增大并发连接数的,一个是网络设备队列,一个是TCP S队列


红色的几条其实也很有用,主要是对keepalive的连接,但这次没用上就不多讲了。昨晚这些后网络连接情况就大有好转

  • 2000多个ESTABLISHED

  • 200+ 个TIME_WAIT

  • 100+ 个CLOSE_WAIT


可即使这样,Redis服务器还是负载很高,这个时候另一个噩耗也从市场部穿了过来。人家EXO是有国际刷票组织的~~~~~。 人家韩国也有一大票粉丝(瞧,了解你的用户有多重要!!!)


不行,我们要帮唯一一台缓存服务器减压。这次我们没有采用纯技术的手段而是通过了一些变通方式,我们发现了一些规律:

  1. 这次的明星按照其知名度其实是拉开了距离了的,第一和第二的很接近,粉丝一直在互相厮杀。但是和第三,第四拉开了不小的距离。同样第三,第四的明星也拉开后面的票数距离。

  2. 基于#1,我们决定采用分组等价折算票数的方式,处于第一组的明星,我们给予相同的 n:1 的折算比。对第二组,第三组都类似。因此我们要保证的是在同组内的公平。

有了这个思路,就有了第二台Redis 服务器,有点类似计算机里的累加寄存器(AX),当在Redis#02里的累加值达到折算比例时往Redis #01里增加1。有了这台Redis服务器,针对那些热点明星的粉丝热情,首当其冲的流量会由Redis #02承受,因此减轻了Redis #01的压力



最后一天


自从新的调整后,系统一直活得的稳稳的。直到截至投票的最后几个小时,不多说啥问题了,上图,满满的都是泪
来自粉丝们的最后的疯狂... ...

再看看iostat,一样的疯狂


Redis 你是怎么了? 大家注意到了发生CLOSE_WAIT的进程了吗? redis-rdb-bgsa,这个是异步保存数据的进程,而且这个进程会定时发生。是不是和Redis的两种持久化机制有关(aof,rdb)?


好吧,果然

aof的原理,类似于预写日志。其中几个选项如下:


  • appendfsync always:总是写入aof文件,并完成磁盘同步

  • appendfsync everysec:每一秒写入aof文件,并完成磁盘同步

  • appendfsync no:写入aof文件,不等待磁盘同步。


可见,从持久化角度讲,always是最安全的。从效率上讲,no是最快的。而redis默认设置进行了折中,选择了everysec。合情合理。

bgrewriteaof机制,在一个子进程中进行aof的重写,从而不阻塞主进程对其余命令的处理,同时解决了aof文件过大问题。


现在问题出现了,同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形,现在no-appendfsync-on-rewrite参数出场了。如果该参数设置为no,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题。如果设置为yes呢?这就相当于将appendfsync设置为no,这说明并没有执行磁盘操作,只是写入了缓冲区,因此这样并不会造成阻塞(因为没有竞争磁盘),但是如果这个时候redis挂掉,就会丢失数据。丢失多少数据呢?在linux的操作系统的默认设置下,最多会丢失30s的数据。


写在最后


四天三夜并不长, 预估错误也并不可怕。但这个经历如何在活动过程用最小成本,最小影响的支撑活动继续进行的确让我学到了不少。 因此分享出来供大家借鉴拍砖,有很多决策回想起来可能会有更好的替代方案,就留待下一次活动去尝试。

 
大房说 更多文章 年过三十,开始码字 获取知识的渠道 最近读的几本好书 开通了知乎专栏 我所亲见的前端技术变革
猜您喜欢 IPv6迁移过程当中踩的坑 中国软件开发者白皮书 iOS 9学习:UIStackView如何让你的开发更简单 可视化图表表达的10个错误 移动开发每周阅读清单:App Analytics上线、Google Play试用