Comments

写完代码测试通过之后,终于松一口气,然后可以愉快的部署上线了。但是问题随之而来:如何部署?或者如何能更自动化的部署?

部署应用是一系列的操作,就环境而言,分为本地和远程服务器,就操作而言,大概包括提交代码、备份代码、更新代码、安装依赖、迁移数据库、重启服务等流程。其中除了提交代码这一步是在本地完成,其余操作都需要在服务器环境执行。

上面的流程当中,有一个很重要的,就是如何同步代码(提交、备份、更新)。就我的经验,了解或用过这些方式:

  • rsync: rsync 是一个文件同步的工具,如果配置好使用起来体验也不错。但是有很多缺点:
    • 配置复杂,命令行参数多
    • 需要在服务器上运行 rsyncd,默认监听 873 端口(可能会有防火墙)
  • scp: scp 底层用的是 SSH 协议,所以只要服务器上运行了 sshd 就可以双向 copy 文件。对于文件传输来说,scp 比 rsync 体验差的地方有:
    • 不能增量更新,每次都是全部传输
    • 不能配置忽略文件(.git 怎么办?)
  • git: 就个人而言,git 是最方便的部署方式了,有版本控制,可以增量更新,可以配置忽略文件,使用简单。实际上只要有可能,都推荐用 git 来发布代码。但问题在于,很多公司的 git 服务器都是在内网的,所以在服务器上无法访问。

很幸运的是,我们有一个公网可以访问的 git 服务器,所以可以用 git 来发布代码。发布完代码后就是后续的一系列操作了,最原始的方式,是登录到服务器,然后一步一步敲命令执行下来。但是如果要频繁部署的话(快速迭代时肯定要经常更新代码),这就变成了繁复的体力劳动,而且容易出错(漏了流程,看花眼了)。于是就想到了脚本,把这些操作写成 shell 脚本,然后执行脚本就好了。这是一个很大的进步,然而仍然存在一个问题:从本地环境到远程环境,需要登录,导致了流程上的阻断。

Fabric 是 Python 编写的一个可以实现自动化部署和系统维护的命令行工具,只需要写一些简单的 Python 代码就能轻松解决上面提到的所有问题。Fabric 底层用的是 SSH 协议,提供了一系列语义清晰的 API 来组合实现部署任务。

Read on →

Supervisor (http://supervisord.org) 是一个用 Python 写的进程管理工具,可以很方便的用来启动、重启、关闭进程(不仅仅是 Python 进程)。除了对单个进程的控制,还可以同时启动、关闭多个进程,比如很不幸的服务器出问题导致所有应用程序都被杀死,此时可以用 supervisor 同时启动所有应用程序而不是一个一个地敲命令启动。

安装

Supervisor 可以运行在 Linux、Mac OS X 上。如前所述,supervisor 是 Python 编写的,所以安装起来也很方便,可以直接用 pip :

sudo pip install supervisor

如果是 Ubuntu 系统,还可以使用 apt-get 安装。

Read on →
Comments

缘起

GFW 早已经是臭名昭著,路人皆知的了,因为它的存在,使得整个大陆的用户都只能在「局域网」里活动。政治敏感的内容就不说了,很多技术性的网站也被墙掉,导致查找问题浏览网页时经常网络被重置。

我是个重度 Google 用户,虽然经常用到的 Google 的产品基本上只有 Google 搜索和 Gmail,但只需要这两项就让我离不开 Google。此外,还有很多网站使用 Google 的 OpenID 登录,引用 Google 的字体文件和其他资源文件,这些网站也都几乎无法正常访问。我曾经使用过一些手段来实现翻墙,在大学时得益于教育网免费的 IPv6,毕业后使用了很久的 GoAgent,手机上用过 fqrouter,然而都不是很稳定和一劳永逸的解决方案。

有很多人使用 VPN,有购买的,也有自己搭建的。在 GoAgent 无法使用后,我开始正式考虑使用 VPN 了,但不想买 VPN,主要原因有:

  1. 很多人使用的 VPN 容易被盯上而面临被干掉的危险(应该是多虑了)
  2. 出于信息安全和隐私的考虑,不希望自己的信息有被第三方获取的风险(所以也不想用 fqrouter 了)
  3. 想自己折腾

所以就选择了国外 VPS + Shadowsocks 的解决方案。

Read on →
Comments

平时需要经常用到 SSH,比如登录远程服务器,用 Git 推送和更新代码等。建立一次 SSH 连接可能并不需要多久长时间,但是如果要频繁登录同一台服务器,就未免显得有些繁琐和浪费时间。如果是用用户名和密码登录,每次都要输入密码就更加让人崩溃。还有使用 Git 的时候,短时间内可能需要经常 git pullgit push,如果每次操作都需要重新建立连接,等待过程就让人心生厌恶了。

实际上,SSH 有个「鲜为人知」的特性可以做到重用连接,只有在第一次登录的时候会创建新的连接,后续的会话都可以重用这个已经存在的连接。这样,后续的登录就会非常快,而且不需要输入密码认证。配置也很简单,直接上代码。

修改 ~/.ssh/config 文件,添加如下配置:

Host *
    ControlMaster auto
    ControlPath /tmp/ssh_mux_%h_%p_%r
    ControlPersist 600
Read on →
Comments

Golang 里面 map 不是并发安全的,这一点是众所周知的,而且官方文档也很早就给了解释:Why are map operations not defined to be atomic?. 也正如这个解释说的一样,要实现一个并发安全的 map 其实非常简单。

并发安全

实际上,大多数情况下,对一个 map 的访问都是读操作多于写操作,而且读的时候,是可以共享的。所以这种场景下,用一个 sync.RWMutex 保护一下就是很好的选择:

type syncMap struct {
    items map[string]interface{}
    sync.RWMutex
}

上面这个结构体定义了一个并发安全的 string map,用一个 map 来保存数据,一个读写锁来保护安全。这个 map 可以被任意多的 goroutine 同时读,但是写的时候,会阻塞其他读写操作。添加上 GetSetDelete 等方法,这个设计是能够工作的,而且大多数时候能表现不错。

但是这种设计会有些性能隐患。主要是两个方面:

  1. 读写锁的粒度太大了,保护了整个 map 的访问。写操作是阻塞的,此时其他任何读操作都无法进行。
  2. 如果内部的 map 存储了很多 key,GC 的时候就需要扫描很久。
Read on →
Comments

Golang 的 defer 语句是个非常有用的语法,可以把一些函数调用放到一个列表里,在函数返回前延迟执行。这个功能可以很方便的在函数结束前处理一些清理操作。比如关闭打开的文件,关闭一个连接,解锁,捕捉 panic 等。

这篇 Go Blog 用例子讲解了 defer 的用途和使用规则。总结一下主要就是三点:

  • 传递给 defer 语句的参数是在添加时就计算好的。比如下面的函数的输出将会是 0.
func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
  • 多个 defer 语句的执行顺序类似于 stack,即 Last In First Out. 比如下面的函数的输出将会是 3210.
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
  • defer 语句可能会读取并修改函数的命名返回值(named return values)。比如下面的函数的返回值将会是 2 ,而不是 1.
func c() (i int) {
    defer func() { i++ }()
    return 1
}
Read on →
Comments

严格的讲,应该是在把 intfloat等类型转换为字符串时,不要用 fmt.Sprintf,更好的做法是用标准库函数。fmt.Sprintf 的用途是格式化字符串,接受的类型是 interface{},内部使用了反射。所以,与相应的标准库函数相比,fmt.Sprintf 需要更大的开销。大多数类型转换的函数都可以在 strconv 包里找到。

int to string

整数类型转换为字符串,推荐使用 strconv.FormatIntint64),对于 int 类型,strconv.Itoa 对前者做了一个封装。

比较一下 strconv.FormatIntfmt.Sprintf 的时间开销:

package main

import (
    "fmt"
    "strconv"
    "time"
)

const LOOP = 10000

var num int64 = 10000

func main() {
    startTime := time.Now()
    for i := 0; i < LOOP; i++ {
        fmt.Sprintf("%d", num)
    }
    fmt.Printf("fmt.Sprintf taken: %v\n", time.Since(startTime))

    startTime = time.Now()
    for i := 0; i < LOOP; i++ {
        strconv.FormatInt(num, 10)
    }
    fmt.Printf("strconv.FormatInt taken: %v\n", time.Since(startTime))
}

其中某一次运行结果:

fmt.Sprintf taken: 2.995178ms
strconv.FormatInt taken: 1.057318ms
Read on →
Comments

Interface 接口

Go 语言标准库提供了排序的package sort,也实现了对 intfloat64string 三种基础类型的排序接口。所有排序调用 sort.Sort,内部根据排序数据个数自动切换排序算法(堆排、快排、插排)。下面这段代码出自 Go 标准库 sort/sort.go:

func quickSort(data Interface, a, b, maxDepth int) {
    for b-a > 7 {
        if maxDepth == 0 {
            heapSort(data, a, b)
            return
        }
        maxDepth--
        mlo, mhi := doPivot(data, a, b)
        // Avoiding recursion on the larger subproblem guarantees
        // a stack depth of at most lg(b-a).
        if mlo-a < b-mhi {
            quickSort(data, a, mlo, maxDepth)
            a = mhi // i.e., quickSort(data, mhi, b)
        } else {
            quickSort(data, mhi, b, maxDepth)
            b = mlo // i.e., quickSort(data, a, mlo)
        }
    }
    if b-a > 1 {
        insertionSort(data, a, b)
    }
}

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
    // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
    n := data.Len()
    maxDepth := 0
    for i := n; i > 0; i >>= 1 {
        maxDepth++
    }
    maxDepth *= 2
    quickSort(data, 0, n, maxDepth)
}
Read on →
Comments

innodb_flush_log_at_trx_commitsync_binlog 是 MySQL 的两个配置参数,前者是 InnoDB 引擎特有的。之所以把这两个参数放在一起讨论,是因为在实际应用中,它们的配置对于 MySQL 的性能有很大影响。

1. innodb_flush_log_at_trx_commit

简而言之,innodb_flush_log_at_trx_commit 参数指定了 InnoDB 在事务提交后的日志写入频率。这么说其实并不严谨,且看其不同取值的意义和表现。

  1. innodb_flush_log_at_trx_commit 取值为 0 的时候,log buffer 会 每秒写入到日志文件并刷写(flush)到磁盘。但每次事务提交不会有任何影响,也就是 log buffer 的刷写操作和事务提交操作没有关系。在这种情况下,MySQL性能最好,但如果 mysqld 进程崩溃,通常会导致最后 1s 的日志丢失。
  2. 当取值为 1 时,每次事务提交时,log buffer 会被写入到日志文件并刷写到磁盘。这也是默认值。这是最安全的配置,但由于每次事务都需要进行磁盘I/O,所以也最慢。
  3. 当取值为 2 时,每次事务提交会写入日志文件,但并不会立即刷写到磁盘,日志文件会每秒刷写一次到磁盘。这时如果 mysqld 进程崩溃,由于日志已经写入到系统缓存,所以并不会丢失数据;在操作系统崩溃的情况下,通常会导致最后 1s 的日志丢失。

上面说到的「最后 1s」并不是绝对的,有的时候会丢失更多数据。有时候由于调度的问题,每秒刷写(once-per-second flushing)并不能保证 100% 执行。对于一些数据一致性和完整性要求不高的应用,配置为 2 就足够了;如果为了最高性能,可以设置为 0。有些应用,如支付服务,对一致性和完整性要求很高,所以即使最慢,也最好设置为 1.

Read on →
Comments

这其实是我之前在 StackOverflow 上回答过的一道题,令我感到意外的是,这个问题只有我一个人回答,而且我也获得了 8 个赞同。小小的成就感。

1. What

原题在这里:How to validate integer range in Flask routing (Werkzeug)?

简单翻译一下,大致如下:

Flask 应用里面有一个这样的路由

from foo import get_foo

@app.route("/foo/<int:id>")
def foo_id(id):
    return render_template('foo.html', foo = get_foo(id))

其中 id 的取值是 1~300,如何在路由层级做这个验证?也就是一个类似于这样的东西 @app.route("/foo/<int:id(1-300)").

Read on →
getElementsByTagName('BODY')[0]).appendChild(s); }()); getElementsByTagName('BODY')[0]).appendChild(s); }()); getElementsByTagName('BODY')[0]).appendChild(s); }());