阳春三月在成都

这是一篇月末总结,如果你是不小心被标题吸引误入,那么我更加建议你把这篇月志读完。既来之,则安之😁

三月工作上发生了挺大变化,很多老朋友离开了,很多新同学加入了。无论是离开还是加入的人,里面都有自己喜欢和欣赏的人。成都的圈子只有这么大,鄙人都祝福他们。

自己又回到3年前的类似位置,做着类似的事情,此乃物是。三年前犯过的错误、苦恼纠结的事情,无一缺席,但是已经没有了当初面对类似事情的不确定性,此乃人非。

这个月重新找到了写字的愉悦感。在某些平台居然还被特别推荐过,我跟妞儿说这是一件funny的事情,没有开心的成分。算是把延期了好几年的课程补上了一小步。

在这个过程中,鄙人也开始逐渐体会到了自己订阅的公众号作者写作的内心感受。写作的目的可能有很多种,但是对于某一类人来说,写作就是一种习惯性的分配特定时间进行思考。这种思考可能在写作开始之前已经在潜意识中发生。写作就是讲这些思考的尘埃落定和梳理的过程。另外,根据我的观察,一般擅长写作的人,大多都是逻辑清晰且沟通起来很愉快的人。

三月挺幸运,包括今天。接触到了很多交流起来很愉快的人。鄙人跟妞儿说,遇到聪明的人的时候,有一种发自内心的欣赏和热爱。这种热爱跟基情没有关系,对于没有那么熟络的人,珍惜交流的效率和时间;对于熟悉的人,会聊很多平时不会聊的话题和看法。要知道,其实这个世界上,能够一起沟通和讨论一些深度想法的人太少太少了,遇到一个,怎能不喜?有那么一群人,注定就是寂寞。

马上而立之年了,责任随不至于越来越重,但是却只越来越真实。这个月似乎也硬着头皮处理了很多家庭上的事情。也许有时候过于追求效率,以及当面揭穿一些人的套路伎俩吧,给人的感觉是脾气太冲,经常惹恼很多人,包括家人。在这个点上,我从来不抱怨什么路长且累呀,什么不理解呀。我挺同意韩路的观点:如果你对目标足够清晰和强壮,那么做一些事的时候必然会有取舍和付出。想通了这件事,很多事情就是理所当然的,哪里还有什么抱怨。我花了挺长时间想清楚这个问题,也会有心情郁闷的时候,也会有到家以后,熄火在车里闷两分钟的时候,但是只要踏入家门,我必然是女儿的好爸爸,妞儿的好丈夫,父母的好儿子,而这一切,内心是举重若轻,毫无纠结的。

三月:

  • 最喜欢的物品:飞行棋手机架
  • 最喜欢的书:《原则》
  • 最惺惺相惜的人:至今不知道他的名字,但是却有过一次非常深入、愉快的交流
  • 财务目标一个都没有达到,且存在浮亏的款项
  • 独立项目新开了一个项目,看能走多远吧

三月开始,南方逐渐进入一年中最好的日子。在期待四月中,2018已经过去25%.

一次非典型性 Redis 阻塞总结

南方逐渐进入一年中最好的时节,用户也开始骚动起来。看了眼数据,活跃用户已经double很远,马上triple了。

一日睡眼惺忪的清晨,正看着数据默默yy时候,线上开始告警…… MMP,用户早上骚动的增长比想象好快呢。同事第一时间打开立体监控瞥了一眼,结合服务的错误日志,很快把问题锁定到了一个Redis实例(事实上,自从立体监控上线以后,基本上处理流程从以前的 < 80%时间定位问题 + 20%解决问题 > 变成了 < 少量时间确认问题 + 解决问题 >)。团队处理效率还是挺快的,原因定位到AOF持久化:

这是当时的Redis配置:

127.0.0.1:6379> config get *append*
1) "no-appendfsync-on-rewrite"
2) "no"
3) "appendonly"
4) "yes"
5) "appendfsync"
6) "everysec"

从配置看,原因理论上就很清楚了:我们的这个Redis示例使用AOF进行持久化(appendonly),appendfsync策略采用的是everysec刷盘。但是AOF随着时间推移,文件会越来越大,因此,Redis还有一个rewrite策略,实现AOF文件的减肥,但是结果的幂等的。我们no-appendfsync-on-rewrite的策略是 no. 这就会导致在进行rewrite操作时,appendfsync会被阻塞。如果当前AOF文件很大,那么相应的rewrite时间会变长,appendfsync被阻塞的时间也会更长。

这不是什么新问题,很多开启AOF的业务场景都会遇到这个问题。解决的办法有这么几个:

  1. no-appendfsync-on-rewrite设置为yes. 这样可以避免与appendfsync争用文件句柄,但是在rewrite期间的AOF有丢失的风险。
  2. 给当前Redis实例添加slave节点,当前节点设置为master, 然后master节点关闭AOF,slave节点开启AOF。这样的方式的风险是如果master挂掉,尚没有同步到salve的数据会丢失。

我们采取了折中的方式:在master节点设置将no-appendfsync-on-rewrite设置为yes,同时添加slave节点。

理论上,问题应该解决了吧?啊蛤,的确是理论上。

修改后第一天,问题又出现了。惊不惊喜,意不意外?

于是,小伙伴又重新复习了一下当时出问题时候的Redis日志:

有两个点比较可以:

  1. 前几条AOF日志告警日志发生在晚上3~5点之间,而那个时候,我们整个系统负载是非常低的。
  2. 清晨的告警日志不是某一个Redis实例告警,而是该机器上的所有Redis实例都在告警。

在这种百思不得骑姐的情况下,结合历史上被坑的经验,我们99%断定是我们使用的云主机存在问题。

这个问题有可能是宿主机超售太多导致单个租户实际能使用到的云盘IO比标称值低,也有可能是租户隔离做得不好,导致坏邻居过度占用IO影响其他租户。

这个很好理解:我们使用的是阿里云的云SSD,而阿里云目前的架构还没有做到计算和存储分离,即计算和存储的网络IO是共享的。

当然目前这个问题还没有实锤,我们也还在跟阿里云积极沟通解决。同时为了避免给自己惹麻烦,我还是留了1%的其他可能性😅

祝大家周末愉快!

参考资料

Redis相关—Redis持久化

Dropbox Bandaid 微服务反向代理/Sevice Mesh 代理解析

随着微服务架构以及的广泛普及,很多公司都会使用或者自行开发自己的API Gateway, 甚至在内部服务也会应用Service Mesh.

不久前,看到了一篇Dropbox公司介绍其内部服务代理Bandaid的文章: Meet Bandaid, the Dropbox service proxy. 不得不说设计细节一贯独角兽风格,非常有收获,对于改进我们自己设计的proxy 也有一定参考意义。这里做一个简单的读后笔记。英语好的同学可以直接阅读原文。(插一句,Dropbox不久前在美股上市了,在中美贸易战中为数不多逆势上涨的股票之一,玩美股的同学可以关注一下。再插一句:股市有风险,投资须谨慎。)

Bandaid 诞生的背景

Bandaid 是有公司内部的反向代理服务演变而来,使用Golang实现。反向代理有很多成熟的解决方案,之所以选择自行开发主要有以下几个原因:

  • 更好的与与内部的基础设施集成
  • 可以复用公司内部基础库(更好的与内部代码集成)
  • 减少对外部的依赖,团队可以灵活的按需开发
  • 更适合公司内某些特殊使用场景

上面的大部分因素根我们进行微服务组件开发时候的考量基本一致。这也是我们当初没有使用Go kit 这种工具套装进行架构微服务改造的顾虑,当然,那个时候还没有这些工具链。

Bandaid 的特性

  • 支持多种负载均衡策略 (round-robin, least N random choices, absolute least connection, pinning peer)
  • 支持 https to http
  • 上下游支持 http2
  • 路由改写
  • 缓存请求与响应
  • host级别的逻辑隔离
  • 配置热加载
  • 服务发现
  • 路由信息统计
  • 支持gRPC代理
  • 支持HTTP/gRPC健康检查
  • 流量支持按权重分配和金丝雀测试

丰富的负载均衡策略,以及对HTTP/2和gRPC的支持实例亮点。要知道,nginx 从 1.13.10才开始支持gRPC.

另一方面,从支持的特性看,如果要求不是特别多,直接用来作为Service Mesh也是相当不错的。而且由于是使用Go开发,对于本来就在使用Go作为技术栈的团队来说,无论是使用还是二次开发,门槛和学习成本都是很低的。

Bandaid 设计解析

整体架构

请求队列设计

接收的请求按照LIFO 后入先出的方式进行处理。这个设计有点反直觉,但却是合理的:

  • 绝大部分情况下,队列应该是空或者接近空的状态。因此LIFO的策略并不会恶化队列最大等待时间。
  • 根据业务类型,可以配置队列的优先级和长度。可以非常方便的实现服务限流、服务降级和熔断器。
  • Bandaid 采用总是接收TCP连接,并将连接交由用户态管理的策略。结合LIFO有一个很大的好处:
    • 与内核态管理连接比较,如果客户端发送请求后意外关闭了TCP连接,Bandaid 是无法马上获取到该错误的,需要等到读取完该请求,然后处理请求后开始写response时才会触发错误,发现这个连接其实已经关闭。因此,处理这类请求是在无谓的消耗服务器资源。而采用LIFO和用户态管理连接的话,Bandaid 可以根据配置的超时策略,一定程度上drop掉这类请求,减少处理这种「dead request」的数量。

Worker 设计

Worker采取固定大小的工作池设计,一方面可以精确的控制并发数量,另一方面,也避免频繁创建worker的开销。不过文中也承认,池的大小在设置的时候需要结合业务考虑清楚,否则可能不能充分利用服务器资源,错误的触发服务降级。

worker在处理队列中的请求时,支持按照优先和权重处理。因此,可以非常容易的实现金丝雀发布和逻辑上的upstream隔离。

负载均衡策略

  • RR, 均匀撒胡椒面。优点是足够简单,缺点是没有考虑不同后端服务实例的数量和接口处理时长差异,会导致大量Bandaid 服务资源被极少数的慢服务和接口消耗掉。
  • least N random choices: 先随机选择N个候选upstream host, 然后选择连接数最少的host (认为是当前负载最低的)作为最终目标host.

这种方式在在大部分时候可以工作得很好,但对于那种快速失败的小服务会失效。因为这种服务有很大概率被选中,但是并不意味着其当前负载较低。缓解该策略的方式是absolute least connection.

  • absolute least connection: 从全局host中选择连接数最少的host作为目标host.

  • pinning peer: 将worker与host绑定。这种方式可以避免慢服务过量消耗Bandaid资源的问题,但是请求调度不够灵活。

总结

从 Bandaid 的设计看,无论是作为 reverse proxy 还是 service mesh 都有不错的潜力。不过 Dropbox 团队当前还没有公开Bandaid性能测试数据,代码也还没有开源。因此,猴急的同学可能还需要等一段时间。

论一个合格的车载手机支架的自我修养之什么是最好的车载手机支架

作为一个酷爱驾驶的人和一名手机党,给座驾安装一个手机支架是非常重要的事情。然而,要寻找一个还用的车载手机支架真的是一件比想象要困难的多的事情。

手机支架自己买过很多了。但是都在使用一段时间后(甚至体验几分钟后)发现不能接受的设计问题而丢垃圾桶。

大概5年前,购买了第一个手机支架,他大概长这样(姑且称之为第一代车载手机支架吧):

这个手机架使用上没有什么太大问题,功能上一直表现不错,因此使用了挺长一段时间。但是,后来换的手机屏幕更大了,继续使用这个手机支架的话会使得手机上沿会挡住汽车自己的一部分显示屏。此外,这个手机支架下端只依靠一个塑料架作为之巅,把出风口的下方已经磕出了历史的痕迹😅

于是换了当时最流行的磁吸式车载手机支架:

这种支架需要在手机或者手机壳上安装一个贴片(也叫做引磁片),然后往空调出风口上的支架一放就可以靠磁力吸住。应该说,使用传统的支架换到这个磁吸支架的头两天,心里还是默默为发明这种之家的人点赞的。

它的支架结构非常简单,在车里非常简洁,而且调整引磁片的位置是的手机固定在一个自己最喜欢的位置。但是,这种简洁却是以牺牲手机的外观为代价的。且不说很多引磁片上面有品牌logo, 且logo极丑,关键是无论多么薄的引磁片贴在手机上以后都让以前光滑平整的手机背面不在平整,非常变扭。

另一方面,在使用一段时间以后,发现指南针和陀螺仪都不再准确,不过导航似乎一直都还正常。推测是这两个部件因为长期使用这种强磁设备影响已经被磁化。作为一个相信科学的人居然在这里翻车,想来也是挺自嘲的一件事情。

然而,事实是「磁吸式手机之间是否影响手机」这个事情目前都没有定论。我在购买前也担心过这个事情。专门在蛤乎还转了一圈,我估计大家找到的大概也是这个答案:

然是有影响的,表面磁力超过一定范围.现在市面上的手机基本上都用到了磁铁,其内部隔磁都做到了200GS以下,越好的手机内部隔磁越好.

所以,对于手机支架,表面磁力(接触手机的部分)超过200GS就会对手机产生影响,甚至损害.就现在市面上的磁性手机支架而言,都在2000GS以上,是标准值的10倍以上.

这样导致的结果就是干扰手机的电磁信号和磁化手机内部的含铁部件,逐渐导致收集导航的失准和失灵、通话音质的失真、运行卡顿、数据丢失、屏幕花屏或者黑屏、间歇性死机,直至报废。

以上的现象一般会在1到4个月内相继出现,根据手机的不同也会出现不同的情况。这个也有专业的测试机构,但鉴定需要最少一年的时间才能判别对手机的影响到底多大,只有专业的磁铁生产厂家和手机生产厂家能自己用仪器测出具体值。

建议购买磁性支架应选择盖上铁片(贴在手机背面的那片,没有不行)后表面磁力在200GS以下的。

给一个简单的测试方法:在手机支架上盖上铁在手机背面的铁片,把一圆硬币从上面滚落,不会吸住的基本上都只有几百GS,过大的都会吸住硬币,则不建议购买。

但是,请相信我,我也做过这个硬币实验,硬币滚落了,于是我以为自己购买的产品是OK的。但是现实却是打脸的……

我看过很多所谓汽车媒体的评测,结论这种磁铁不影响手机。理由无非亮点:

  1. 厂家使用了多颗磁铁,形成的是闭合磁场,因此厂家肯定是做了充分测试的。
  2. 苹果的smart cover外设也是使用磁铁作为屏幕感应开关的,因此磁铁是不会影响电子设备的。

最开始,我还挺相信上面的结论的。但是,后来一想两个理由都有点滑稽:

  1. 学过初中物理的都应该知道,这个所谓的闭合磁场是限制磁场在一个立体区域,引磁片才一毫米厚,闭合个屁呢。另外,生产这这些支架的厂商都是一些作坊而已,这里根本没有什么信用可以背书。
  2. 苹果的确在smart cover中使用了磁铁,但是你发现那个磁铁在iPad屏幕的最右侧边缘位置了吗?而你的引磁片是贴在近乎手机中心的位置……而长期跟磁场如此近距离接触,含铁的零件被磁化无非是快慢的事罢了

抛弃了磁吸式手机支架后,我有购买了最时尚的重力感应手机支架:

第一眼看到的时候,有没有觉得创意很赞呀?最开始我也是这样认为的。于是我一股脑买了两个。但是……拿到手后就不得不说,这什么鬼呀……

所谓成也重力,败也重力。因为使用重力收紧两边的手臂夹紧手机,因此三个臂都是可以活动的。由于,手机重量一般只有一百多克,因此三个臂基本没有阻尼。这就意味着,整个手机支架非常松垮。尤其是你没有在支架上放手机的时候,开起来的过坑洼路段的时候感觉整个车都要散架一样。

这个结构的手机支架跟品牌没有关系,设计原理导致它的噪音是无解的。淘宝上到处刷评的第一卫一样垃圾,一样有这个问题。

找呀找呀找支架,于是我找到了这个目前为止最满意的车载手机支架

有没有感觉又回到了最古老的那款手机支架的感觉?这就对了。那款支架我使用时间最长,而这款支架把我之前认为的设计缺陷都解决掉了:

  1. 从结构看,它把第一款手机支架的固定方式修改为与磁吸式的固定方式一致,加入了软性塑料的夹子及不会产生异响,同时也不会在支架下部支点位置处留下历史的痕迹。
  2. 将固定手机的方框结构修改为圆形无下部支脚的弹力感应结构,这就解决了老款支架手机上下不可调,导致遮挡车载屏幕的问题。

总结下来就是,它既有普通支架的通用可靠,同时又有磁吸式支架的简洁灵活,并且不需要在手机上安装引磁片,免去磁铁影响手机的担忧。

这款手机支架使用已经有段时间,等过段时间有一次长途体验以后在来盖棺定论。

100行代码实现基于 QUIC 的 http 代理

本站开启支持 QUIC 的方法与配置后,主观感觉从国内访问快了很多。看了一下Chrome的timing, 大部分建立连接都能够做到0-RTT:

既然这样,顺手实现一个基于QUIC的http代理,把平时查资料时使用的网络也顺带加速一下。(对了,前两天看到Google发布了Outline, 看来这项运动从来都不缺少运动员哪……)

http 代理原理

http 代理处理http和https请求的方式有所不同。对于http请求:

  1. 浏览器与代理服务器建立TCP连接后,将http请求发送给代理服务器。
  2. 代理服务器将http请求发送给目标服务器。
  3. 代理服务器获取到相应结果以后,将结果发送给浏览器。

这里有一个细节需要注意,浏览器向代理服务器发送的http请求URI与直接访问有所不同。

浏览器直接访问 GET http://www.yahoo.com 的http请求格式为:

GET / HTTP/1.1
User-Agent: Quic-Proxy
...

而向代理服务器发送的http请求格式为:

GET http://www.yahoo.com HTTP/1.1
User-Agent: Quic-Proxy
...

也就是浏览器想代理服务器发送的http请求URI中包含了scheme和host,目的是为了让代理服务器知道这个代理请求要访问的目标服务器地址。

对于https请求,一般是通过CONNECT建立隧道:

  1. 浏览器向代理服务器建立TCP连接,发送CONNECT请求。
  2. 代理服务器根据CONNECT请求中包含的host信息,向目标服务器建立TCP连接,然后向浏览器返回200连接成功的响应。
  3. 这时代理服务器同时维持着连接浏览器和目标服务器的TCP连接。
  4. 从浏览器的角度看,相当于建立了一条直连目标服务器的TCP隧道。然后直接在该隧道上进行TLS握手,发送http请求即可实现访问目标服务器的目的。

QUIC Proxy的设计与实现

QUIC Proxy 部署结构图

QUIC Proxy的部署结构与上面http代理原理稍微有所不同。主要区别是增加了qpclient。主要原因是应用程序与代理服务器支架的请求是明文传输(http请求代理是全明文,https请求代理时的CONNECT头会泄露目标服务器信息)。我们是要隐私的人(虽然小扎可能并不care),因此,在应用程序与qpserver之间加了一个qpclient,之间使用QUIC作为传输层。

实现

QUIC Proxy使用Go实现,猴急的同学可以直接到github看源码:Quic Proxy, a http/https proxy using QUIC as transport layer.

代码比较简单,基于标准库的http.Server根据http代理的原理进行了一点http请求的修改。然后,因为qpclientqpserver之间使用QUIC作为transport,而QUIC上的每一个connection都是可以多路复用(multiplexing)的,因此,对于qpserver需要自己实现一个传入http.Server的listener:

type QuicListener struct {
    quic.Listener
    chAcceptConn chan *AcceptConn
}

type AcceptConn struct {
    conn net.Conn
    err  error
}

func NewQuicListener(l quic.Listener) *QuicListener {
    ql := &QuicListener{
        Listener:     l,
        chAcceptConn: make(chan *AcceptConn, 1),
    }
    go ql.doAccept()
    return ql
}

func (ql *QuicListener) doAccept() {
    for {
        sess, err := ql.Listener.Accept()
        if err != nil {
            log.Error("accept session failed:%v", err)
            continue
        }
        log.Info("accept a session")

        go func(sess quic.Session) {
            for {
                stream, err := sess.AcceptStream()
                if err != nil {
                    log.Error("accept stream failed:%v", err)
                    sess.Close(err)
                    return
                }
                log.Info("accept stream %v", stream.StreamID())
                ql.chAcceptConn <- &AcceptConn{
                    conn: &QuicStream{sess: sess, Stream: stream},
                    err:  nil,
                }
            }
        }(sess)
    }
}

func (ql *QuicListener) Accept() (net.Conn, error) {
    ac := <-ql.chAcceptConn
    return ac.conn, ac.err
}

同样的,qpclientqpserver建立连接也需要考虑到多路复用的问题,实现实现一个基于QUIC的dialer:

type QuicStream struct {
    sess quic.Session
    quic.Stream
}

func (qs *QuicStream) LocalAddr() net.Addr {
    return qs.sess.LocalAddr()
}

func (qs *QuicStream) RemoteAddr() net.Addr {
    return qs.sess.RemoteAddr()
}

type QuicDialer struct {
    skipCertVerify bool
    sess           quic.Session
    sync.Mutex
}

func NewQuicDialer(skipCertVerify bool) *QuicDialer {
    return &QuicDialer{
        skipCertVerify: skipCertVerify,
    }
}

func (qd *QuicDialer) Dial(network, addr string) (net.Conn, error) {
    qd.Lock()
    defer qd.Unlock()

    if qd.sess == nil {
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: qd.skipCertVerify}, nil)
        if err != nil {
            log.Error("dial session failed:%v", err)
            return nil, err
        }
        qd.sess = sess
    }

    stream, err := qd.sess.OpenStreamSync()
    if err != nil {
        log.Info("[1/2] open stream from session no success:%v, try to open new session", err)
        qd.sess.Close(err)
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil)
        if err != nil {
            log.Error("[2/2] dial new session failed:%v", err)
            return nil, err
        }
        qd.sess = sess

        stream, err = qd.sess.OpenStreamSync()
        if err != nil {
            log.Error("[2/2] open stream from new session failed:%v", err)
            return nil, err
        }
        log.Info("[2/2] open stream from new session OK")
    }

    log.Info("addr:%s, stream_id:%v", addr, stream.StreamID())
    return &QuicStream{sess: qd.sess, Stream: stream}, nil
}

好吧,我承认实现代码似乎在200行左右……但是,我们实现了一个client和一个server, 平均下来基本控制在100行左右,对吧……(😂逃……)

部署

:需要golang版本 >= 1.9

1. 在远程服务器上安装 qpserver

go get -u github.com/liudanking/quic-proxy/qpserver

2. 启动qpserver:

qpserver -v -l :3443 -cert YOUR_CERT_FILA_PATH -key YOUR_KEY_FILE_PATH

3. 在本地安装 qpclient

go get -u github.com/liudanking/quic-proxy/qpclient

4. 启动 qpclient:

qpclient -v -k -proxy http://YOUR_REMOTE_SERVER:3443 -l 127.0.0.1:18080

5. 设置应用程序代理:

以 Chrome with SwitchyOmega 为例:

Enjoy!

本站开启支持 QUIC 的方法与配置

在越来越讲究用户体验的今天,网络带宽的提高已经很难有显著的页面加载改善,而低延迟的优化往往能够起到意想不到的效果。在《TLS1.3/QUIC 是怎样做到 0-RTT 的》中我们分析了TLS1.3和QUIC在低延迟方面的原理和低延迟优势。在从源代码编译 nginx docker 镜像开启 TLS 1.3中我们已经把玩了TLS1.3,没有理由不把玩一下QUIC,对吧?

起初以为,在普及程度上,QUIC因为主要是Google主导,会曲高和寡。但是,查了一下,发现腾讯早在2017年就在生产环境应用了QUIC:让互联网更快的协议,QUIC在腾讯的实践及性能优化. 结果显示:

灰度实验的效果也非常明显,其中 quic 请求的首字节时间 (rspStart) 比 http2 平均减少 326ms, 性能提升约 25%; 这主要得益于 quic 的 0RTT 和 1RTT 握手时间,能够更早的发出请求。

此外 quic 请求发出的时间 (reqStart) 比 h2 平均减少 250ms; 另外 quic 请求页面加载完成的时间 (loadEnd) 平均减少 2s,由于整体页面比较复杂, 很多其它的资源加载阻塞,导致整体加载完成的时间比较长约 9s,性能提升比例约 22%。

既然大厂都已经发车,我司也就可以考虑跟进了。稳妥起见,决定先在自己的博客开启QUIC,然后再逐步在线上业务进行推广。

方案概览

方案非常简单:不支持QUIC的浏览器依旧通过nginx tcp 443访问;支持QUIC的浏览器通过caddy udp 443访问。

由于nginx近期没有支持QUIC的计划, 作为一名gopher, 因此这里选择caddy作为QUIC的反向代理。后面会介绍caddy的具体安装和配置方法。

对于支持QUIC的浏览器来说,第一次访问支持QUIC的网站时,会有一次服务发现的过程。服务发现的流程在QUIC Discovery
有详细介绍。概括来说,主要有以下几步:

  1. 通过TLS/TCP访问网站,浏览器检查网站返回的http header中是否包含alt-svc字段。
  2. 如果响应中含有头部:alt-svc: 'quic=":443"; ma=2592000; v="39"',则表明该网站的UDP 443端口支持QUIC协议,且支持的版本号是draft v39; max-age为2592000秒。
  3. 然后,浏览器会发起QUIC连接,在该连接建立前,http 请求依然通过TLS/TCP发送,一旦QUIC连接建立完成,后续请求则通过QUIC发送。
  4. 当QUIC连接不可用时,浏览器会采取5min, 10min的间隔检查QUIC连接是否可以恢复。如果无法恢复,则自动回落到TLS/TCP。

这里有一个比较坑的地方:对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。没有什么特殊的原因,提案里面就是这么写的。具体的讨论可以参见Why MUST a server use the same port for HTTP/QUIC?

从上面QUIC的发现过程可以看出,要在网站开启QUIC,主要涉及两个动作:

  1. 配置nginx, 添加alt-svc头部。
  2. 安装和配置QUIC反向代理服务。

配置nginx, 添加alt-svc头部

一行指令搞定:

安装QUIC反向代理服务器caddy

上面我们提到对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。然而,caddy服务器的QUIC特性无法单独开启,必须与TLS一起开启,悲剧的是TLS想要使用的TCP 443端口已经被nginx占用了😂

场面虽然有点尴尬,但是我们有docker:将caddy安装到docker中,然后只把本地的UDP 443端口映射到容器中即可。

于是我们创建了一个docker-caddy项目。Dockerfile 10行内搞定:

caddy 服务配置文件/conf/blog.conf:

启动docker:

开启Chrome浏览器QUIC特性

chrome://flags/中找到Experimental QUIC protocol, 设置为Enabled. 重启浏览器生效。

测试QUIC开启状态

重新访问本站https://liudanking.com, 然后在浏览器中打开:chrome://net-internals/#quic。如果你看到了QUIC sessins,则开启成功:

当然,你也可以给Chrome安装一个HTTP/2 and SPDY indicator(An indicator button for HTTP/2, SPDY and QUIC support by each website) 更加直观的观察网站对http/2, QUIC的支持情况。

从源代码编译 nginx docker 镜像开启 TLS 1.3

nginx最近更新挺频繁的,其中TLS 1.3和是HTTP/2 Server Push是两个比较有意思的特性。前者可以有效的减少握手次数,降低延迟,尤其在恢复会话时候可以将握手开销降低到 0-RTT,后者则可以通过服务器主动推送资源,可以认为是在资源预加载之上更进一步的「资源主动加载」,有效提高网页性能和用户体验。

考虑到平时自己需要比较频繁的测试不同的nginx的新版本,但是又不想破坏机器本地已经安装好的nginx环境,因此我决定制作一个nginx docker镜像。需要说明的是,dockerhub官方是有一个nginx镜像的,但我们希望自己定制nginx版本和需要开启的模块,因此该镜像并不符合需求。

这里以开启TLS 1.3为例制作一个以ubuntu:16.04为基础镜像的 nginx dockerm镜像。顺便把上次在Ubuntu 14.04开启nginx http2支持的方法中埋的坑填了🙂

nginx开启TLS 1.3的前置条件

  1. nginx >= 1.13.0
  2. openSSL >= 1.1.1 alpha

openSSL版本有几点点需要注意:

  1. 以前可以使用分支tls1.3-draft-18编译支持TLS 1.3,但是现在已经merge到1.1.1版本中了,因此,这里不再推荐使用tls1.3-draft-18编译nginx. 使用openSSL 1.1.1编译nginx会触发nginx的一个config bug: undefined reference to 'pthread_atfork', 解决办法可以参见这里以及nginx mailing list, 这里暂时回退到使用tls1.3-draft-18.
  2. 不要尝试使用tls1.3-draft-19编译nginx, 使用Chrome 64测试无法在nginx 1.13.9上成功开启TLS 1.3, 应该跟Chrome对draft版本的支持有关系。
    • 2018-03-14更新:升级到Chrome 65以后,发现默认支持为TLS 1.3 Draft 23, 后面顺带连通config bug一起解决了再更新。
  3. openSSL目前最新版本是OpenSSL_1_1_1-pre2, 还不是stable版本,因此,不要在生产环境中使用。

Dockerfile

Nginx 开启 TLS1.3

一个完整的配置模板参考如下:

主要新增两个修改:

  1. ssl_protocols添加TLSv1.3.
  2. ssl_ciphers加入TLS13_为前缀的密码套件。

完整项目地址:docker-nginx

验证TLS 1.3

  • Chrome: 将 chrome://flags/ 中的 Maximum TLS version enabled 改为 Enabled (Draft).

如果你刚刚升级到了Chrome 65,选项中开启TLS1.3有三个选项:Enabled(Experiment 2), Enabled(Draft 22), Enabled (Draft 23). 同时很遗憾的告诉你,本文中使用的是Draft 18编译nginx,因此无法配合Chrome 65无法开启TLS1.3,请下面介绍Firefox的开启方法测试TLS1.3🤣

  • Firefox: 将 about:config 中的 security.tls.version.max 改为 4.

以Chrome 64为例,如果开启成功后Protocol显示TLS 1.3:

你也可以访问tls.liudanking.com来测试自己的浏览器是否已经成功开启TLS 1.3.

参考资料

本博客开始支持 TLS 1.3

TLS1.3/QUIC 是怎样做到 0-RTT 的

《低延迟与用户体验杂谈》中,我们提到了TLS1.3以及QUIC协议可以有效的降低传输层的延迟,改善用户体验。而TLS1.3和QUIC降低用户延迟有一个共同点,那就是在提供同等功能特性下,减少Round-Trip Time (RTT)次数。你甚至会经常听到QUIC和TLS1.3的拥趸说TLS1.3和QUIC可以做到0-RTT,基本消灭了(安全)传输层的延迟。事实真的是这样吗?

TLS RTT

我们先看一下TLS1.2 (以ECDH为例)的建立加密连接的过程:

从上图可以看出,在发送应用数据之前,TLS安全传输层需要经过2-RTT才能完成握手。如果是恢复会话,TLS1.2可以采用Session IDSession Ticket进行快速握手:

从上图可以看到,在使用Session Ticket的情况下,需要1-RTT。

那么TLS1.3是如何进一步优化的呢?

TLS 1.3完全握手

在完全握手情况下,TLS 1.3需要1-RTT建立连接。与TLS1.2有两点不同:握手过程中移除了ServerKeyExchangeClientKeyExchange, DH (Diffie-Hellman) 参数通过 key_share 传输。

TLS1.3恢复会话握手

TLS1.3恢复会话可以直接发送加密后的应用数据,不需要额外的TLS握手。因此,我们常说的TLS1.3 0-RTT握手其实是指恢复加密传输层会话不需要额外的RTT。但是,在第一次进行完全握手时,是需要1-RTT的。

与TLS1.2比较,无论是完全握手还是恢复会话,TLS1.3均比TLS1.2少1-RTT。因此,TLS1.3从被提上draft草案开始获得了各方面的好评。从nginx和Chrome支持此特性的速度可见一斑。

但是TLS1.3也并不完美。TLS 1.3 O-RTT无法保证前向安全性(Forward secrecy). 简单讲就是,如果当攻击者通过某种手段获取到了Session Ticket Key,那么该攻击者可以解密以前的加密数据。

这个问题不是TLS1.3独有,TLS1.2也存在这个问题。对于TLS1.3来说,要缓解该问题可以通过设置ServerConfiguration中的Expiration Date字段,使得与Session Ticket Key相关的DH静态参数在短时间内过期(一般几个小时)。

使用TLS最多的场景是HTTPS和HTTP/2. 以HTTP/2为例,一个完整的 HTTP Request需要经历的RTT如下:

HTTP/2 over TLS1.2首次连接 HTTP/2 over TLS1.2连接复用 HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT 1-RTT 0-RTT
TLS握手 2-RTT 1-RTT 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 5RTT 2-RTT 4-RTT 1-RTT

从上表可以看出:

  1. 首次连接发起HTTP请求是非常昂贵的。因此,如果你是用HTTPS作一些不可告人的代理应用的话,一定尽量保持长连接,避免频繁建立新连接。
  2. 连接的multlplexing(多路复用)可以有效减少RTT次数,降低延迟。在连接复用的情况下,TLS1.3将整个http request的RTT降低到了1个,达到理论的最小值。

QUIC RTT

  1. 当客户端首次发起QUIC连接时,客户端想服务器发送一个client hello消息,服务器回复一个server reject消息。该消息中有包括server config,类似于TLS1.3中的key_share交换。这需要产生1-RTT. 事实上,QUIC加密协议的作者也明确指出当前的QUIC加密协议是「注定要死掉的」(destined to die), 未来将会被TLS1.3代替。只是在QUIC提出来的时候,TLS1.3还没出生😂,这只是一个临时的加密方案。
  2. 当客户端获取到server config以后,就可以直接计算出密钥,发送应用数据了,可以认为是0-RTT。密钥推导可以参见Key derivation.
  3. 因此,QUIC握手除去首次连接需要产生1-RTT,理论上,后续握手都是0-RTT的。
  4. 假设1-RTT=100ms, QUIC建立安全连接连接的握手开销为0ms, 功能上等价于TCP+TLS, 但是握手开销比建立普通的TCP连接延迟都低:

    (正常体为首次建立连接的延迟,粗体部分为后续握手的延迟)

HTTP/2 over TLS 1.3 vs QUIC

当前,QUIC普遍的应用场景是 HTTP/2 over QUIC. 我们以一个完整的 HTTP Request需要经历的RTT为例,比较TLS1.3与QUIC的RTT开销:

HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用 HTTP/2 over QUIC首次连接 HTTP/2 over QUIC连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT
TLS握手 1-RTT 0-RTT
QUIC握手 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 4RTT 1-RTT 3-RTT 1-RTT

从上表可以看出:HTTP/2 over QUIC相比HTTP/2 over TLS 1.3最大的优势是首次连接是的RTT开销降低了1-RTT(25%)。在连接复用的情况,QUIC的RTT开销与TLS1.3相等。

前面我们提到,QUIC(UDP+QUIC Crypto)在功能层面等价于TCP+TLS, 并且其加密协议(QUIC Crypto)未来会被TLS1.3代替。而在连接复用的情况下QUIC与TLS1.3的RTT开销旗鼓相当,那么是否意味着在HTTP/2的应用场景下,TCP+TLS1.3就可以完全替代QUIC呢?答案是否定的,主要原因如下:

  1. QUIC从数据包级别解决了队头阻塞的问题,而TCP+TLS只能解决http request级别的队头阻塞问题。具体原理可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么》.
  2. QUIC与HTTP/2结合更加紧密,比如HTTP/2的stream, frame, Header Compression都可以直接映射到QUIC的stream, packet, Header Compression. 从协议分层看这会使得应用层和传输层紧耦合,但是在HTTP/2这个具体场景下,QUIC可以接管HTTP/2的拥塞控制,相较于HTTP/2 over TLS 1.3会受到TCP和HTTP/2两层的拥塞控制来说,无疑具有更直接有效的控制策略。并且,QUIC的拥塞控制是应用层可插件化的。虽然你也可以为TCP编写拥塞控制模块,但那是一个内核模块,稍不注意就会让系统崩溃。如果你有兴趣的话,实现一个QUIC的BBR拥塞控制模块是很容易的。
  3. QUIC支持连接迁移(Connection Migration),即在客户端切换网络IP以后,也能够保持连接不间断。这对于移动网络这种漫游网络特别友好,再加上QUIC在首次连接上RTT比TLS1.3低25%,因此会有更好的用户体验。虽然Multipath TCP, MPTCP也可以吃支持连接迁移,但这受限于操作系统和网络协议栈,要想普及,任重道远。
  4. QUIC支持Forward Error Correction,利用一定的数据包冗余,提供丢包时恢复丢包数据的能力。减少了包重传的数量,进而减少延迟,提高带宽利用率。如果你使用过kcptun,你会发现它也采用了FEC,对于弱网和高丢包环境下,效果尤其好。

当然,QUIC也是有缺点的。比如很多运营商对UDP的QoS级别是比TCP低的,甚至只允许DNS 53端口的UDP包,其他UDP包都是被封掉的。在IDC中,UDP的优先级一般也比TCP低。因此,即使以后大规模普及QUIC,也不会完全替代TCP+TLS,毕竟需要确保在QUIC不可用时,可以回落到TCP+TLS.

总结

在首次连接的时候,无论是TLS1.3还是QUIC都需要1-RTT,在连接复用的情况下,两者才能做到0-RTT. 在HTTP/2的应用场景中,QUIC可以有效的降低首次连接的RTT次数,并且支持连接迁移、FEC以及更加灵活高效的拥塞控制,因此可以在移动网络、弱网环境提供低延迟的用户体验。

扩展阅读

Golang 获取 goroutine id 完全指南

在Golang中,每个goroutine协程都有一个goroutine id (goid),该goid没有向应用层暴露。但是,在很多场景下,开发者又希望使用goid作为唯一标识,将一个goroutine中的函数层级调用串联起来。比如,希望在一个http handler中将这个请求的每行日志都加上对应的goid以便于对这个请求处理过程进行跟踪和分析。

关于是否应该将goid暴露给应用层已经争论多年。基本上,Golang的开发者都一致认为不应该暴露goid(faq: document why there is no way to get a goroutine ID),主要有以下几点理由:

  1. goroutine设计理念是轻量,鼓励开发者使用多goroutine进行开发,不希望开发者通过goid做goroutine local storage或thread local storage(TLS)的事情;
  2. Golang开发者Brad认为TLS在C/C++实践中也问题多多,比如一些使用TLS的库,thread状态非常容易被非期望线程修改,导致crash.
  3. goroutine并不等价于thread, 开发者可以通过syscall获取thread id,因此根本不需要暴露goid.

官方也一直推荐使用context作为上下文关联的最佳实践。如果你还是想获取goid,下面是我整理的目前已知的所有获取它的方式,希望你想清楚了再使用。

  1. 通过stack信息获取goroutine id.
  2. 通过修改源代码获取goroutine id.
  3. 通过CGo获取goroutine id.
  4. 通过汇编获取goroutine id.
  5. 通过汇编获取goroutine id.

在开始介绍各种方法前,先看一下定义在src/runtime/runtime2.go中保存goroutine状态的g结构:

其中goid int64字段即为当前goroutine的id。

1. 通过stack信息获取goroutine id

原理非常简单,将stack中的文本信息”goroutine 1234″匹配出来。但是这种方式有两个问题:

  1. stack信息的格式随版本更新可能变化,甚至不再提供goroutine id,可靠性差。
  2. 性能较差,调用10000次消耗>50ms。

如果你只是想在个人项目中使用goid,这个方法是可以胜任的。维护和修改成本相对较低,且不需要引入任何第三方依赖。同时建议你就此打住,不要继续往下看了。

2. 通过修改源代码获取goroutine id

既然方法1效率较低,且不可靠,那么我们可以尝试直接修改源代码src/runtime/runtime2.go中添加Goid函数,将goid暴露给应用层:

这个方式能解决法1的两个问题,但是会导致你的程序只能在修改了源代码的机器上才能编译,没有移植性,并且每次go版本升级以后,都需要重新修改源代码,维护成本较高。

3. 通过CGo获取goroutine id

那么有没有性能好,同时不影响移植性,且维护成本低的方法呢?那就是来自Dave Cheney的CGo方式:

文件id.c:

文件id.go:

完整代码参见junk/id.

这种方法的问题在于你需要开启CGo, CGo存在一些缺点,具体可以参见这个大牛的cgo is not Go. 我相信在你绝大部分的工程项目中,你是不希望开启CGo的。

4. 通过汇编获取goroutine id

如果前面三种方法我们都不能接受,有没有第四种方法呢?那就是通过汇编获取goroutine id的方法。原理是:通过getg方法(汇编实现)获取到当前goroutine的g结构地址,根据偏移量计算出成员goid int的地址,然后取出该值即可。

项目goroutine实现了这种方法。需要说明的是,这种方法看似简单,实际上因为每个go版本几乎都会有针对g结构的调整,因此goid int64的偏移并不是固定的,更加复杂的是,go在编译的时候,传递的编译参数也会影响goid int64的偏移值,因此,这个项目的作者花了非常多精力来维护每个go版本g结构偏移的计算,详见hack目录。

这个方法性能好,原理清晰,实际使用上稳定性也不错(我们在部分不太重要的线上业务使用了这种方法)。但是,维护这个库也许真的太累了,最近发现作者将这个库标记为“DEPRECATED”,看来获取goroutine id是条越走越远的不归路😂

5. 通过汇编获取goroutine id

虽然方法4从原理和实际应用上表现都不错,但是毕竟作者弃坑了。回到我们要解决的问题上:我们并不是真的一定要获取到goroutine id,我们只是想获取到goroutine的唯一标识。那么,从这个角度看的话,我们只需要解决goroutine标识唯一性的问题即可。

显然,上面作者也想清楚了这个问题。他新开了一个库go-tls, 这个库实现了goroutine local storage,其中获取goroutine id的方式是:用方法4的汇编获取goroutine的地址,然后自己管理和分配goroutine id。由于它获取到的并不是真正的goroutine id,因此我将之称为goroutine id。其实现的核心代码如下:

  1. 获取g结构地址。
  2. 分配伪goroutine id.

这种方式基本没有什么不能接受的hack实现,从原理上来说也更加安全。但是获取到不是你最开始想要的goroutine id,不知你能否接受😅

小结

获取goroutine id是一条不归路,目前也没有完美的获取它的方式。如果你一定要使用goroutine id,先想清楚你要解决的问题是什么,如果没有必要,建议你不要走上这条不归路。尽早在团队中推广使用context, 越早使用越早脱离对goroutine id的留恋和挣扎。

Credit

理工男是如何选择iPhone 8 (Plus)和iPhone X的

上一台手机是iPhone 6 Plus 64G, 使用到现在将近快3年半。6p在升级iOS 10以后,速度就算不上流程了,使用微信和支付宝的时候尤其明显。特别是使用支付宝付款的时候,经常卡在story board画面,没少受后面排队哥们白眼。

现在(2018.03)并不算是购买苹果这代手机最好的时机,因为一般最好的价格都是出现在双十一。而且本着早买早享受的原则,现在入手其实体验周期也间接变短了。这次考虑换机原因只有一个:6p实在是不能战斗下去了。虽然并不喜欢这代的iPhone,但是,也只能矮子里面拔将军了:选iPhone 8 (Plus)还是iPhone X?

习惯性的打开哈乎看了一下,纠结着两个选项的人好像还挺多的:

  • 你会买 iPhone 8 还是 iPhone X?
  • 你会选择买iPhone X还是iPhone 8,或者其他手机?
  • iPhone 8 (Plus) 和 iPhone X 你会买哪个?为什么?
  • iphone X 和 iphone 8/8plus 哪个更值得买?
  • iPhone 8/8p/X,不知道选哪个?

其实上面的问题只会越看越不知道自己选什么,因为每个人都有自己的需求和背景。

这里我就不卖关子,直接给出自己的选择:iPhone 8 Plus, 容量看自己的需要,因为自己的6p是64G且容量将满,因此选的是256G。主要理由如下:

face id解锁交互用在了错误的场景

face id解锁是一个自己无论如何也无法接受的交互方式。我仔细体验过同事的iPhone X,成功率尚可,但是做不到「存在于无形之中」。人脸解锁如果做不到这一点,就是一个半成品。

这一点类似于汽车行业之前大规模使用触屏代替物理按键(呃,好吧,马斯克同学还在坚持在特斯拉车型中大规模使用触屏……),不是说触屏控制本身存在技术障碍,而是在这个场景中使用了一个高成本且不合适的技术。

我甚至无法想象晚上在被窝中要解锁iPhone X时,必须要让手机的主动探照光闪瞎狗眼的酸辣情景。那么,什么场景是适合face id的呢?我认为是在结合其他id因子的安全领域,比如在大额支付时,使用touch id和face id进行双重校验。

iPhone X的全面屏是个笑话,全面屏最终都会变成笑话

齐刘海问题肯定算不上优点,但是问题并不大(好吧,其实苹果开发者还是挺吐槽这个刘海带来的额外开发工作量的……)。重点是,全面屏在当前阶段并不是一个手机尺寸上能够提升用户体验的发展方向。

以前手机尺寸的发展方向是变薄,有些厂家甚至以此作为卖点。但是几年下来,还有哪个厂家将自家机器薄作为卖点?就连以前乔帮主健在时,发布会现场直击从文件袋里拿出来的iPad Air也不在突出它薄的数值了。

空口无凭,我们看一下苹果历代16款手机的厚度数据对比:

结果是不是很意外?

  • 从iPhone 5开始,苹果手机的厚度基本就没有太大变化。
  • iPhone 6是迄今为止,苹果最薄的手机。
  • iPhone 6开始,苹果手机厚度是逐年上升的!
  • 更加有意思的是,iPhone X是水果这5年来最厚的手机😂

那么这个问题跟全面屏有什么关系呢?我们先看一下全面屏的定义:

全面屏从字面上解释就是手机的正面全部都是屏幕,手机的四个边框位置都是采用无边框设计,追求接近100%的屏占比。但其实到目前为止没有一个严格意义上的全面屏幕定义,而比较主流的看法是要有18:9的屏幕,正面取消任何实体或者触摸按键,只有一块完整的屏幕,而且屏幕占比至少要达到80%以上。显然目前有很多手机虽然也号称全面屏幕,但屏幕占比却没有达到这个要求,所以看上去边框很宽,并没有特别好的效果。

所以,仔细想想,你就发现,在现阶段各家手机屏占比越做越高的今天,全面屏已经基本成为或接近事实,并不需要为了迎合这个概念而调整手机本来已经验证多年的设计,尤其是正面的按键和触感器排列。这一点上,锤子手机罗永浩是深谙其道的,他发布的坚果 2 Pro手机宣传语就很准确「Almost 全面屏」😝

因此,当前阶段,行业对全面屏的疯狂追捧跟当年恨不得手机能够用来切菜的追逐是如此神似。当然,也有头脑清醒的人只是跟着喊口号,但是并没有真的跳进坑里,比如老罗。

手机是需要握持使用的,两边的边框是一定会有所保留的,无论是iPhone X还是小米的MIX系列。有意思的是,这两个厂家疯狂宣传他们全面屏的同时,却没有告诉你,他们把手机两旁的边框做了轻微加高和立面加厚处理,当你进行从屏幕边缘划入屏幕操作时,感觉手指被一个圆铁丝顶了一下一样,还不如以前「正常屏」手机顺滑。而iPhone X的全面屏还有一个额外的槽点:话说你提高屏占比是为了显示更多的信息吧,但是你看看iPhone X现在的屏幕显示情况,很多情况下屏幕下方很大一截都是空的,要这铁棒有何用?

小结苹果的全面屏:

  • 全面屏是一个伪概念,在当今屏占比越来越高的今天,全面屏就是个概念和幌子。
  • 在屏占比提到到一定程度以后,对用户体验影响的提高就很小了,甚至会降低用户体验。
  • 如果一定要为了全面屏而改变正面的一些布局,无疑是舍本逐末、因小失大的做法。

iPhone X的优点

但并不是说iPhone X就全无优点。

结合其深度传感器使用的Animoji可能如当初Emoji一样影响整个行业,前提是深度传感器能够迅速降低成本并普及。

摄像头双重防抖对于喜欢拍照的同学来说更是锦上添花。

OLED屏幕显示效果主观观感的确非常赞。

选择iPhone 8 Plus的困扰

  • 没有辨识度。其实换手机有好几天了,但是同事根本没有发现我换了手机。但是,iPhone早就过了可能装X的年代了,所以也没觉得损失什么。
  • 多年使用同一个设计ID的产品。6p用了三年多,8p估计也会用2年以上,意味着6年都用着一个外观的产品。这点自己并不排斥,要知道设计不是一直向前的,有时候还会轻微倒退。正如我一直觉得ThinkPad T61/T400的设计是一代经典,因此我现在还在用着这10年前的笔记本电脑做一些简单的文档处理和网页浏览。
  • 手机重量历代之最,用久了手腕疼。但是有些人觉得这才是质感
  • 玻璃后盖易碎。但是有些人认为这才叫温润如玉。

其实上面说的都谈不上困扰,只是你选了8p以后需要考虑能否接受的小妥协。

为什么不选择iPhone 7 (Plus)?

选iPhone 7 (Plus)的唯一原因当然是价格。但是,作为一名理工男+参数党,通过研究苹果这一代的芯片发现,iPhone 7可能并不适合自己。

从上图可以发现,这两代CPU的制程一个是16nm, 一个是10nm. (苹果历代CPU参数可以参见这里。 无论是理论还是实际测试,A11芯片都比A10芯片能效更高,续航时间更长。而iPhone 8 Plus更是做到了在电池容量比iPhone 7 Plus少10%左右的情况下,续航反而增加了一个多小时。

另一方面,A11采用的是两个高性能+4个能效核的设计,比A10多两个效能核。理论上A11的CPU跑分会更高。使用Geekbench 4也应证了这一点:

单核性能提升23%,多核性能提升76%!

之于iPhone 8 Plus加入的无线充电,聊胜于无吧。倒是快充功能在紧急场合能够发挥一点作用。

另一方面,由于自己换机频率不高,因此,对于自己而言,iPhone 8 Plus更加适合自己。而且这几天购买正好碰到3.8女王节活动,直降1000,性价比也不算低。

总结

不选iPhone X有很多理由,选择iPhone 8 Plus则是因为被动选择。虽然当前的iPhone X只是一个半成品,但是库克肯定认为它是以后苹果手机的发展方向,自己对下一代iPhone X也没有任何期待。因此iPhone 8就是苹果以iPhone 6为设计语言的一代产品的绝唱了,这也算是自己选8的一个原因。