Golang协程之殇

什么是协程?

协程(Coroutine)是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一个概念。而且协程的概念是早于线程(Thread)提出的。但是由于协程是非抢占式的调度,无法实现公平的任务调用。也无法直接利用多核优势。因此,我们不能武断地说协程是比线程更高级的技术。

尽管,在任务调度上,协程是弱于线程的。但是在资源消耗上,协程则是极低的。一个线程的内存在MB级别,而协程只需要KB级别。而且线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小。

我们把协程的基本特点归纳为:

1. 协程调度机制无法实现公平调度
2. 协程的资源开销是非常低的,一台普通的服务器就可以支持百万协程。

那么,近几年为何协程的概念可以大热。我认为一个特殊的场景使得协程能够广泛的发挥其优势,并且屏蔽掉了劣势 — 网络编程。

与一般的计算机程序相比,网络编程有其独有的特点。

1. 高并发(每秒钟上千数万的单机访问量)
2. Request/Response。程序生命期端(毫秒,秒级)
3. 高IO,低计算(连接数据库,请求API)。

最开始的网络程序其实就是一个线程一个请求设计的(Apache)。后来,随着网络的普及,诞生了C10K问题。Nginx 通过单线程异步IO把网络程序的执行流程进行了乱序化,通过IO事件机制最大化的保证了CPU的利用率。

至此,现代网络程序的架构已经形成。基于IO事件调度的异步编程。其代表作恐怕就属NodeJS了吧。

异步编程的槽点

异步编程为了追求程序的性能,强行的将线性的程序打乱,程序变得非常的混乱与复杂。对程序状态的管理也变得异常困难。写过Nginx C Module的同学应该知道我说的是什么。

我们开始吐槽NodeJS 那恶心的层层Callback。

Golang

再我们疯狂被NodeJS的层层回调恶心到的时候,Golang 作为名门之后开始走入我们的视野。并且迅速的在Web后端极速的跑马圈地。其代表者Docker 以及围绕这Docker展开的整个容器生态圈欣欣向荣起来。其最大的卖点 – 协程 开始真正的流行与讨论起来。

我们开始向写PHP一样来写全异步IO的程序。看上去美好极了,仿佛世界就是这样了。

在网络编程中,我们可以理解为Golang 的协程本质上其实就是对IO事件的封装,并且通过语言级的支持让异步的代码看上去像同步执行的一样。

安全正确的使用Golang协程

我们在使用Golang的时候不一定就要非常深入的理解与学习协程的种种技术细节。但我们得时时刻刻的警惕起来。因为无论我们的代码写得是多么的同步化(看上去所有的IO好像都是阻塞的),但其本质还是异步。在协程里面会进行各种各种的中断与调度。

  • 谨慎的使用全局变量

当我们在定义一个全局变量的时候,我们就是在埋下一颗炸弹。它可能会以你想象不到的姿势点燃并爆炸。最容易出错的就是协程竞争与数据污染。

协程竞争

协程竞争指的是不同的协程同时对一个对象(尤其是原生的map)进行数据读写操作。golang 在出现这种竞争的时候将会抛出fatal错误,并且程序退出。

无论是操作structintstring mapslice 都请做好资源锁。

我建议的全局变量应该是只读的,最进行一次初始化。通常在main函数packageinit函数中执行。
数据污染

数据污染指的是程序经过一段运行时间之后,全局变量里面的值变得与期望值不一样。

  • 协程饿死

由于协程是非抢占的式的调度,那么就有可能某一个协程长时间的运行一个协程,导致其他协程无法被调度,直到饿死。

这样的情况,通常发生再大规模的处理程序中。比如数据迭代处理,大型多媒体资源处理等生命期长的协程中。

Golang中,提供了runtime.Gosched() 函数来主动挂起当前协程,把CPU出让,让调度器有机会调度其他协程。

1
2
3
4
5
6
7
8
9
10
11

func BigJob() {

for i := 0;i < 100000000;i ++ {
//do something ...
//每执行1W个任务出让CPU
if i % 10000 == 0 {
runtime.Gosched()
}
}
}

总结

Golang 再语言级别提供了协程支持。让异步编程变得更加轻松,但是它并没有解决存在于并发环境(多线程)中的种种问题。我们需要掌握并发环境中编程的技术知识用来规避和解决问题。

我们要非常了解协程的特点与调度模型。并且通过仔细的检验来保证协程尽可能的被公平调度(至少在web系统中,这是一个非常重要的特性)。

LiteJob,一个Golang的本地任务调度器

背景

在当今的互联网环境中,无法无天的瞬间流量与捉襟见肘的硬件资源是很难调和的矛盾(当前,土豪玩家往往会有数十倍乃至数百倍的预留硬件)。那么,作为一个程序员,你需要做的事情就是让这有限的硬件尽可能多的Hold住更多流量。

在笔者的实践中,主要总结了以下几个方向去完成我们的目标。

  • 性能优化。
    • 应用程序性能优化
    • 数据库性能优化
    • 业务逻辑的优化
  • 流量控制
    • 系统流量控制
    • 租户流量控制
  • 流量优先
    • 将流量分为同步流量与可异步流量

由于篇幅问题,本次主要讨论流量优先。

什么是流量优先?

这里,我们举一个最常见的互联网业务的例子来说明什么是优先流量。

假如,我们的业务是这样的:

  1. 用户点击某个按钮,发起Ajax请求
  2. 在请求内需要查询数据库来进行身份验证
  3. 需要向数据库更新一条记录
  4. 需要将该操作日志写入数据库
  5. 需要将该操作进行远程通知(HTTP POST请求)
  6. 需要将该通知结果写入数据库
  7. 告知用户数据更新结果。

对于这样的业务,我们首先需要考虑的是,哪些操作是本次请求相关的,哪些操作不影响本次请求结果,但是与系统相关的。

很显然的,步骤1 2 3 7 是请求相关的,是影响最终结果的。那么,我们认为这些操作是同步逻辑(优先操作),其余操作在资源有限的情况下,需要让位于该操作(示例中的业务,由于同步逻辑简单,异步化处理之后,该接口的响应性能会有数倍提升)。

如何实现同步流量与异步流量

任务队列。这是一个毋庸置疑的答案。而且有很多成熟的开源系统可供选择。但是,这些系统都是已一个TCP服务进行通信与服务的,那么,这里又会有一个潜在的问题。如果任务队列挂了怎么办?

笔者以另外一个角度思考了异步队列的实现。我们需要把我们的任务推送到统一的消息存储中吗?本地任务队列有什么优缺点(这里的本地不是指代localhost127.0.0.1,而是指应用程序内部)?

本地任务队列

本地任务队列指的是在应用程序内部实现的任务队列系统。

  • 优点

    • 安全,可用性高。没有tcp服务带来的连接,网络等问题造成的消息丢失现象。同时,由于处理的是本地任务(函数指针),也不会有任务处理的不确定性。
  • 缺点

    • 无法支持全局状态查询。当我们的应用被部署多份的时候,我们不能简单的获知任务状态。
    • 应用程序带来了状态。由于应用程序需要维护自己的任务队列,带来了状态。这样对应用的更新,部署带来了问题。

本地任务队列就不可用了吗?

关于缺点1,我们可以引入MySQL,Redis等存储引擎来完成全局状态的迁移。

由于缺点2的存在,我们的应用在实例中销毁需要等到任务队列执行完毕之后(但是是支持停机维护的)。

笔者认为,本地任务队列可以完全替代或部分替代远程任务队列系统。部分替代指的是与远程任务队列配合工作(比如做远程任务队列的推送失败重试队列)。

LiteJob

LiteJob是笔者关于本地任务队列的是一个思考结果。使用Golang实现。最核心的还是Golang的协程系统本身就是良好的控制系统

特性

  • 支持持久化到本地,防止因应用崩溃造成消息的丢失。
  • 支持并发处理任务以及并发量控制。
  • 支持任务重入(依赖任务返回参数)。

开发目标

  • 优化调度系统,控制CPU资源消耗
  • 优化持久化,支持秒级持久化
  • 优化消息存储,同时使用文件和内存两种方式用以支撑更多的消息。

Github地址:https://github.com/imiskolee/litejob/

如何将Golang程序部署到Busybox容器中

本文章主要讲解了如何将我们的Golang程序编译部署到中进行超轻量级程序分发运行。

BusyBox是一个的轻量级Linux系统,系统本身只有1MB的大小。非常适合Docker追求的目标与场景。使用BusyBox一定要做到零依赖,是单纯到极致的一个二进制文件。

为了实现纯静态编译Golang程序,我们需要调整我们的编译参数。大部分时候,我们编译golang程序都会采用下面的命令:

1
go build main.go

这样编译的程序其实是有动态依赖的,我们可以利用ldd命令来查看具体的动态库链接详情。

1
 ldd main

linux-vdso.so.1 =\>  (0x00007ffcbc16e000)
libpthread.so.0 =\> /lib64/libpthread.so.0 (0x00007f091ea83000)
libc.so.6 =\> /lib64/libc.so.6 (0x00007f091e6c2000)
/lib64/ld-linux-x86-64.so.2 (0x00007f091eca6000)

这里可以看到,还是有少部分系统动态库被依赖。这样直接在Busybox中部署会报某些依赖库不存在的错误。
我们使用如下命令进行编译试试看:

1
CGO_ENABLED=0 go build -a

此时,我们已经没法使用ldd来探测依赖了,因为已经是一个静态程序了。理论上来说,只要系统的应用程序格式一致,就可以运行该程序了。

示例Dockerfile:

1
2
3
FROM busybox
COPY ./main /usr/local/app/main
CMD [“/usr/local/app/main”]

为什么我使用Ulysses替换Byword作为主要写作平台

我希望我拥有一个怎么样的写作平台?

  1. 关注写作。看着 Microsoft Office Word 上面那一排排与文字排版相关的按钮就有深深的恐惧。我们当然希望我们可以把更多的精力放在写作本身上面。这也是Markdown大行其道的原因。将排版工作交付给信任的专业第三方软件去处理,这也符合Unix的设计哲学。
  2. 文档管理。我们需要进行各种各样的文字写作(邮件、软件开发文档、文学作品,甚至于稍微正式的各种交流)。不同的文档类型需要进行不同的处理。
    1. 私人文档我更希望使用iCloud来进行自动化的同步管理。
    2. 博客则是对一个特定的文件夹进行管理(博客源代码使用github进行托管,通过Hexo进行发布,最终通过Github Pages部署静态代码,公开访问)。
    3. 一些头脑风暴类型的临时文档我则希望存储在一个临时存储空间中。进行定期自动删除
  3. 编程源代码编写方便。

Byword

基于我的写作需求,我开始寻找一款合适的写作平台。我持续使用Byword大约有半年的时间。几乎每个工作日都需要使用它来进行各种文字编写。

  1. 软件API文档
    1. 邮件内容
    2. 思维记录
      显然的,我如此重度的使用这款软件,已经说明了它足够优秀以及我对它的深深喜爱。
  2. 轻量级。当我需要使用它时,我通常的都是使用spotlight直接打开,然后开始直接写作。写完之后可以直接废弃或者另存为到相应的目录。
    1. 安全性。会自动保存写作进程,不会因为软件异常退出而导致内容丢失。
    2. 多种文档格式(HTML,PDF,WORD)导出。

Ulysses

当我遇到了Ulysses之后,经过了短暂的试用之后,我决定抛弃Byword,拥抱Ulysses。从本质上说,Byword仅仅是关注于写作本身的一个工具,能够享受带来的极大写作乐趣。而不同的是,Ulysses则是一个全功能的写作平台。除了常规的进行写作之后,还兼顾文本的管理,分享得职责。

在使用Byword期间,最让我心烦的一件事情便是文档的管理,我没法方便的去管理我的文档,通常需要打开多个Byword窗口进行来回跳动。而Ulysses则解决了我的这一大难点。事实上,我遇上Ulysses,是我在Google中使用 Markdown Manager Software 进行关键词搜索得到的结果。

Ulysses的文档管理方便做的非常出色。

  1. 支持多个文档源。我们可以从自己的iCloud账户中建立,编辑,自动同步我们的文档。我们还打开把一个本地文件夹挂载到我们的文档目录
  2. 自动备份。 保证了文档的安全。预防各种手抖。

Ulysses的其他功能相比Byword都有相应的增强。

  1. 导出格式更加丰富(HTML,PDF,DOCX,EPUB)。
  2. 支持更多的渲染主题,我们可以通过互联网获取到非常多的渲染模板。
  3. 价格。

相关资源

  1. 点击进入Byword官方网站
  2. 点击进入Ulysses官方网站

基于Intellij IDEA构建Rust开发环境

背景

* 本文介绍了一种基于Intellij IDE构建Rust的开发编译环境的方法
* 本文可能会涉及到以下背景知识:
    1. GFW
    2. Sha***Socks
    3. proxy-chians
Rust简介

Rust是一个新兴的系统编程语言。其核心理念是More Safety More Controls(又安全又极具备控制能力)。

Intellij IDEA

Intellij IDEA是一个基于Java的跨平台现代智能IDE系统。它带来了非常舒适的编程开发体验,官方发行版中集成了当前主流编程语言的版本。通过其插件机制,理论上可以支持任何编程语言。

今天我们将使用intellij-rust 这款插件来构建我们的Rust开发环境。

环境初始化
1. 安装[Intellij IDEA](https://www.jetbrains.com/idea/)
2. 安装[intellij-rust](https://github.com/intellij-rust/intellij-rust) 
    1. $  git clone https://github.com/intellij-rust/intellij-rust 
    2. $  cd intellij-rust 
    3. $ ./gradlew buildPlugin
    4. 启动[Intellij IDEA](https://www.jetbrains.com/idea/)
    5. 进入以下对话框:
        1.  File->Setting->Plugins->Install plugin from disk
        2.  选择 intellij-rust/build/distributions/xxxx.zip
        3. 安装成功后将根据提示重新启动IDE
    6. [intellij-rust](https://github.com/intellij-rust/intellij-rust)  插件安装完毕
环境配置

我们在安装完毕intellij-rust之后,已经可以创建rs源文件了,并且可以享受其智能提示等功能,但是,当前的版本中尚未直接集成编译、调试等命令。接下来,我们将构建Rust的自动构建系统。

1. 进入以下对话框:
    1. File->Setting->Tools->External Tools
    2. 点击对话框的左下方+号添加一个自定义框架命令。
    3. 我提供了一组构建工具的配置方案
Name Description Program Parameters Working Directory
Rustc run with rustc /usr/local/bin/rustc \$FileName\$ \$FileDir\$
CargoRun cargo run /usr/local/bin/cargo run \$ProjectFileDir\$
CargoBuild cargo build /usr/local/bin/cargo build \$ProjectFileDir\$
CargoTest cargo test /usr/local/bin/cargo test \$ProjectFileDir\$
CargoFmt cargo fmt /usr/local/bin/cargo fmt \$ProjectFileDir\$

以下命令将会在Tools->External Tools 下面以列表的形式进行显示。配置的更多参考可以阅读官方文档

快捷键配置

我们可以进入以下对话框,完成上述我们定义的自定义命令的快捷键配置:

1. File->Setting->keymap 
总结

我们已经完成了基于Intellij IDEA的Rust开发环境构建,现在你就可以享受其愉悦的体验了。

意外之喜

由于众说周知的原因,我朝的互联网在访问某些非我国的网络节点的时候,不总是那么顺畅,很遗憾的是,cargo包管理器的源地址就在其列表之列。事实上,carg的源码包托管在amazon s3 的美国节点上,部分github包也是托管在其之上。细节不再表述。

当你在使用cargo的时候,因为网络的原因导致体验不那么顺畅的时候,我们可以寻求解决方案。

1. VPN
2. Sh***Socks                                

如果你使用的是Sh***Socks,可以使用proxychains,以使得 Sh***Socks可以支持终端使用。为了使用proxychains,我们可以将我们的自定义命令做如下变更。

Name Description Program Parameters Working Directory
Rustc run with rustc /usr/local/bin/rustc \$FileName\$ \$FileDir\$
CargoRun cargo run /usr/local/bin/proxychains4 /usr/local/bin/cargo run \$ProjectFileDir\$
CargoBuild cargo build /usr/local/bin/proxychains4 /usr/local/bin/cargo build \$ProjectFileDir\$
CargoTest cargo test /usr/local/bin/cargo test \$ProjectFileDir\$
CargoFmt cargo fmt /usr/local/bin/cargo fmt \$ProjectFileDir\$
总结

至此,我们已经完成整个环境的构建以及解决了我国的特色社会主义的特色问题,你可以尽情的享受编程的快乐了。
本文无意去讨论IDE&Editor之间的战争,毕竟,这是一种宗教。