Moon's blog

write the code, change the world.

FishChatServer源码探究

在写im-go的过程中遇到了一些设计上的问题,于是想找目前有的开源im服务的源码看看。FishChatServer2在一些模块设计上和我的思路很相似,有种英雄所见略同的快感,所以选了它(FishChatServer2的拆包方式和我上一篇文章中提到的使用ReadFull的方式是一样的,并且连模块名字都一样叫Codec)

主要看了libnet和server两个模块

libnet, 是所有server的基础公共库,封装了诸如Listen Accept之类的调用

server, 具体的服务,看了一下gateway和access两个服务的实现


gateway服务

gateway.go是gateway服务的入口,其实是一个access服务的负载均衡器,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化对象
gwServer := server.New()
// codec编码、解码器
protobuf := codec.Protobuf()
// 使用libnet封装的api进行Listen
if gwServer.Server, err = libnet.Serve(conf.Conf.Server.Proto, conf.Conf.Server.Addr, protobuf, 0); err != nil {
glog.Error(err)
panic(err)
}
// 通过etcd进行服务发现,每5秒向etcd请求一个access服务器列表,并写入AccessServerList 这个变量中
go job.ConfDiscoveryProc()
gwServer.Loop() // 开始不断循环处理请求

gwServer.Loop()的核心代码在server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func (s *Server) sessionLoop(client *client.Client) {
for {
//读一个包
reqData, err := client.Session.Receive()
if err != nil {
glog.Error(err)
}
if reqData != nil {
baseCMD := &external.Base{}
// protobuf 反序列化
if err = proto.Unmarshal(reqData, baseCMD); err != nil {
if err = client.Session.Send(&external.Error{
Cmd: external.ErrServerCMD,
ErrCode: ecode.ServerErr.Uint32(),
ErrStr: ecode.ServerErr.String(),
}); err != nil {
glog.Error(err)
}
continue
}
// client.Parse方法有一点迷惑性,client.Parse 其实做了解析命令,并执行命令的工作
if err = client.Parse(baseCMD.Cmd, reqData); err != nil {
glog.Error(err)
continue
}
}
}
}

func (s *Server) Loop() {
glog.Info("loop")
for {
// 获取libnet封装的session对象
session, err := s.Server.Accept()
if err != nil {
glog.Error(err)
}
// 生成client对象,里面封装了gateway服务的业务逻辑
go s.sessionLoop(client.New(session))
}
}

client.Parse最终调用了proto_proc.go里的client.procReqAccessServer来执行业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func (c *Client) procReqAccessServer(reqData []byte) (err error) {
var addr string
var accessServerList []string
// 从之前提到的access服务地址数组中获取一个可用的access服务
// 没有看懂为什么要做一次额外的复制数组的操作?
for _, v := range job.AccessServerList {
accessServerList = append(accessServerList, v.IP)
}

// 处理错误情况
if len(accessServerList) == 0 {
if err = c.Session.Send(&external.ResSelectAccessServerForClient{
Cmd: external.ReqAccessServerCMD,
ErrCode: ecode.NoAccessServer.Uint32(),
ErrStr: ecode.NoAccessServer.String(),
}); err != nil {
glog.Error(err)
}
return
}

// 返回一个可用地址
addr = accessServerList[rand.Intn(len(accessServerList))]
if err = c.Session.Send(&external.ResSelectAccessServerForClient{
Cmd: external.ReqAccessServerCMD,
ErrCode: ecode.OK.Uint32(),
ErrStr: ecode.OK.String(),
Addr: addr,
}); err != nil {
glog.Error(err)
}
return
}

到此一次请求就结束了,可用看出代码的结构上非常清晰,很容易就能理解。


libnet

这个模块帮我们屏蔽了大量繁琐的网络细节,接下来就要看一下它的实现了。

api.go入手,这里定义了对外的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
type Protocol interface { 
// Codec 负责通信协议的解析,封装了读写数据的方法
NewCodec(rw io.ReadWriter) Codec
}

type Codec interface {
Receive() ([]byte, error)
Send(interface{}) error
Close() error
}

func Serve(network, address string, protocol Protocol, sendChanSize int) (*Server, error) {
listener, err := net.Listen(network, address) // 终于看到标准库里的东西了
if err != nil {
return nil, err
}
// listener用于Accept, protocol用户处理net.Conn, sendChanSize看上去好像是用来控制发送速率的,不过没有明白为什么需要控制?
return NewServer(listener, protocol, sendChanSize), nil
}

// 客户端连接+带超时的连接
func Connect(network, address string, protocol Protocol, sendChanSize int) (*Session, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
return NewSession(protocol.NewCodec(conn), sendChanSize), nil
}

func ConnectTimeout(network, address string, timeout time.Duration, protocol Protocol, sendChanSize int) (*Session, error) {
conn, err := net.DialTimeout(network, address, timeout)
if err != nil {
return nil, err
}
return NewSession(protocol.NewCodec(conn), sendChanSize), nil
}

跳过客户的部分的实现,探索一下server.go,负责Accept一个连接,并且封装好一个session对象返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (server *Server) Accept() (*Session, error) {
var tempDelay time.Duration
for {
conn, err := server.listener.Accept()
if err != nil {
// 处理Temporary Error应该是参考了goblog里的error-handling-and-go章节
// For instance, a web crawler might sleep and retry when it encounters a temporary error and give up otherwise.
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
time.Sleep(tempDelay)
continue
}
// 感觉直接比较字符串有点太粗暴了? 但应该是没有办法区分的原因
if strings.Contains(err.Error(), "use of closed network connection") {
return nil, io.EOF
}
return nil, err
}
return server.manager.NewSession(
server.protocol.NewCodec(conn),
server.sendChanSize,
), nil
}
}

manager.go用于管理session,会把session根据id mod 32以后,放进对应的map里, 这里使用了lock来保证并发安全, 但golang1.9以后,应该可以用内置的sync.Map替代了

1
2
3
4
5
6
7
8
9
10
11
12
13
14

func (manager *Manager) NewSession(codec Codec, sendChanSize int) *Session {
session := newSession(manager, codec, sendChanSize)
manager.putSession(session)
return session
}

func (manager *Manager) putSession(session *Session) {
smap := &manager.sessionMaps[session.id%sessionMapNum]
smap.Lock()
defer smap.Unlock()
smap.sessions[session.id] = session
manager.disposeWait.Add(1)
}

Session

server在Accept之后,返回的是一个session对象,session负责收发数据,并且实现了优雅退出(gracefully shutdown)

1
2
3
4
5
6
7
8
9
10
11
type Session struct {
id uint64
codec Codec
manager *Manager
sendChan chan interface{}
closeFlag int32
closeChan chan int
closeMutex sync.Mutex
closeCallbacks *list.List
State interface{}
}

优雅退出的实现,先通过CAS设置一下closeFlag, 成功设置的gorutine可以执行清理操作,失败的gorutine返回SessionClosedError

1
2
3
4
5
6
7
8
9
10
11
12
13
func (session *Session) Close() error {
// 如果成功通过CAS设置了closeFlag
if atomic.CompareAndSwapInt32(&session.closeFlag, 0, 1) {
err := session.codec.Close() // 关闭net.Conn
close(session.closeChan) // 退出sendLoop
if session.manager != nil { // 从manager中移除session
session.manager.delSession(session)
}
session.invokeCloseCallbacks() // 执行callback
return err
}
return SessionClosedError
}

发送数据部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (session *Session) sendLoop() {
defer session.Close()
for {
// 使用select语句来保证,关闭closeChan之后可以退出sendLoop
select {
case msg := <-session.sendChan:
if session.codec.Send(msg) != nil {
return
}
case <-session.closeChan:
return
}
}
}

func (session *Session) Send(msg interface{}) (err error) {
// 在每次Send的时候,都会检查closeFlag,实现快速的退出
if session.IsClosed() {
return SessionClosedError
}
if session.sendChan == nil {
return session.codec.Send(msg)
}

// send block, 返回一个异常, 有点粗暴了
select {
case session.sendChan <- msg:
return nil
default:
return SessionBlockedError
}
}

最后

其实本意是想找找有没有关于心跳和连接保持方面的代码,但没有什么收获.不过也看到了很多高质量的实现,例如idgen,粗粗瞟了一眼就发现,应该是使用了雪花算法,此外还有大量微服务的设计,以及一些我很感兴趣的流行开源技术栈(k8s docker etcd hbase kafka)可以看出是一整套经过深思熟虑的系统,决定过年期间要好好看一看这个库,吸收一下营养。

golang tcp拆包的正确姿势

最近在造一个叫im-go的服务,看名字也能猜出来,是一个基于Go的IM服务,因为不想引入任何的依赖库,所以是手写每个模块的。

之前看过Netty,于是也想做一个类似Netty Codec的,用于编码解码的模块, 方便地处理TCP粘包这种细节问题。

在网上做了一番搜索之后,发现排名靠前的实现,要么出乎意料地复杂,要么根本就是完全错误的,例如

出乎意料的复杂:

错误的:

分析一下这个错误的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Decode(reader *bufio.Reader) (string, error) {
lengthByte, _ := reader.Peek(4)
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
if int32(reader.Buffered()) < length+4 {
return "", err
}

// 假设执行到了这里,那么已经成功读取了长度到length这个变量中
pack := make([]byte, int(4+length))
_, err = reader.Read(pack) //这里是不能保证就能完读到length长度的数据的!!
if err != nil {
return "", err
}
return string(pack[4:]), nil
}

我也受了它的误导,基于Peek()做了一个非常复杂的实现

正确的姿势

在翻了翻io和bufio这两个包之后,我找到了ReadFull

ReadFull,就是调用了ReadAtLeast

1
2
3
func ReadFull(r Reader, buf []byte) (n int, err error) {
return ReadAtLeast(r, buf, len(buf))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
if len(buf) < min {
return 0, ErrShortBuffer
}
for n < min && err == nil {
var nn int
nn, err = r.Read(buf[n:])
n += nn
}
if n >= min {
err = nil
} else if n > 0 && err == EOF {
err = ErrUnexpectedEOF
}
return
}

标准库里的ReadAtLeast就非常优雅了,用n记录读取的总字节数,nn是每次读取到的字节数,一看就明白。

基于ReadFull的拆包代码

1
2
3
4
5
6
7
8
9
10
11
func (c *LenthCodec) Decode(conn net.Conn) (bodyBuf []byte, err error) {
lengthBuf := make([]byte, 4)
_, err = io.ReadFull(conn, lengthBuf)
//check error
length := binary.LittleEndian.Uint32(lengthBuf)

bodyBuf = make([]byte, length)
_, err = io.ReadFull(conn, bodyBuf)
//check error
return
}

后端一年(经验与心得)

先简单介绍一下我的经历,最早在学校的时候,是在社团里写php和Java,创业时期写js,oc和Ruby,现在是全职用Rails写后端了。

项目简介

我们的主要业务有两块,社区和电商

整体业务的峰值qps大概在3000,也算是pv过10亿的站点了,后端team有4个人,除了一个八年老司机,其他人参加工作的年限都不是太久。

我们面对的是一个巨大的基于Rails的历史遗留系统,最早的开发成员均已离开,导致我们常常面对遗留代码一脸蒙逼,到处是没有人知道的逻辑,丑陋的实现,以及很多性能跟不上的接口。

与巨石应用的斗争

日常工作的重中之重,就是与这个monolith的战斗!

性能篇

以往每年我们搞活动,服务器都会挂,经济损失不少,所以优化性能,保证活动期间的访问是第一要务。

原来的活动整体设计还是比较科学的,活动页面本身是静态化的,主要瓶颈是商品详情页面。

我们利用redis做了三层cache,解决了这个问题。第一层是数据库的缓存,直接把商品信息缓存到redis里,避免了频繁的数据库访问,第二层是单条数据的渲染缓存,可以理解成一小段html,第三层是整个数据集的渲染缓存。

第二个瓶颈出现在一些静态资源上,全面迁移到云存储解决。做完这两件事之后,上上次活动是我们有史以来第一次,没有挂。

就在我们觉得,优化做的不错的时候,上次活动却又挂了。

要知道我们特意买了新服务器,美滋滋觉得这下稳了,没想到…

上次活动挂的原因有以下几点

  1. redis hmget,我们通过gem提供的API,缓存了一个巨大的省市区列表,但是没有注意到缓存是分离的,获取整个列表,其实就是一条hmget获取所有独立的缓存片段,这个操作block了redis,导致访问极度缓慢。我们紧急把整个列表转成json,直接贴到代码里返回hotfix了这个问题
  2. 突然无法通过redis sential进行连接,这套sential系统是由已经离职的运维搭建的,我们绕开sential直接连接redis,解决了这个问题
  3. fd limit,发现依然是1024,修改后却依然时常502,发现运维修改的是root用户的fd数量…坑爹!
  4. 在支付回调中有一段用于统计的sql,订单量大了以后slow query,block了数据库,我们直接注释了这段可有可无的老代码,解决。

总结一下,对于web应用的场景来说,大都是读多写少,缓存读请求,异步写请求,是我们经常采用的两种效果不错的方式。在数据库层面,对于遗留代码中效率低下的查询进行重写,重点改写了所有N+1查询,对一些逐条插入的语句用batch insert合并写入操作,也有不错的提升。

替换篇

做的比较有意思的事,是写了我们内部用的个推GEM。原来使用的是github上开源的一个GEM,但是已经很久没更新了,无法适应我们的使用需求。我基于个推最新的HTTPS的API,写了一个Ruby的包装。

这里要吐槽的是个推的技术水平。推送服务是做的不错,但API怎么做的这么low。他们定义了一个叫authorize的http header用来传递身份信息…违背了RFC关于HTTP头必须大写开头的规范。一些语言的标准库(Go、Ruby…)会自动帮你把authorize转化成Authorize,导致个推那边一直返回auth error…而个推的接口又是HTTPS的,抓包调试很困难,浪费了我很长时间调试这个问题。

重构篇
重构的主要方针就是拆分,尽可能把功能从巨石应用中拆出去。如果一时半会难以拆分的,代码上也尽可能让逻辑高度内聚,方便以后迁移。

消息系统的重构
消息系统是一个,出点问题没什么,但做得好会非常出彩的功能。我一直觉得,像知乎这种社区的成功,除了内容,很大一部分要归功于消息的体验。目前,我们几乎所有页面,都会展示新消息的数量,导致每次请求都会去主数据库的消息表做count,计算各种消息的数量返回给前端。我正在着手把整个系统迁移到另一个独立的数据库,以后可以作为单独的服务供内部调用,降级限流什么的都很方便。

搜索的重构

原来的搜索是基于Solr的java工程,是一个我们内部没人维护好多年的烂摊子,虽然各方面表现都不错。我们使用了用Elasticsearch替换掉了。

新系统
我新写了内部的财务系统,过程中遇到很多问题,写的也很痛苦,但最终效果还是不错。因为原来的各种报表都是直接基于生产数据库的,对业务会有冲击,新系统写了一个同步模块,可以增量同步订单数据到财务系统的专用数据库,这样就不会对业务带来影响。

遇到的比较大的坑就是内存爆炸。有一些耗时计算我放到了消息对列里,整个worker进程的内存占用疯狂上升。最终发现是Ruby内存模型的特点,分配大量对象,却不进行回收。需要你使用batch处理的方式,切成一小块一小块的数据,一次处理一小块,这样下次计算的时候就可以重用之前申请的那些对象。

另外也通过时间换空间的方式,把加载全部数据做计算,改成了加载部分数据做计算,然后汇总结果这样的方式,极大降低了内存占用,并每天重启worker进程,解决了最主要的内存问题(1G内存的机器…)。

这个项目让我真实感觉到,有些场景真的不是Ruby擅长的领域。Ruby的内存模型,就是尽量分配对象,从不真正回收,只会重用。Ruby VM启动就有大量空对象等着被分配,假如我加载了很多数据,空对象不够用了,VM就向操作系统申请一批内存,用完后也不释放,等着下次重用。而报表计算的最佳场景就是能加载大量数据,算一下结果,算完释放掉内存。

监控
可以看我之前的文章使用ELK构建分布式日志分析系统

代码篇
在日常编码、重构的过程中,经常使用的技术是

  1. 设计模式
  2. 元编程
  3. 自动化测试

运用设计模式,写出符合OOP规范的代码。分割每个类的职责,尽量让各个功能的逻辑内聚,只提供彼此间调用的接口,这是我最近才刚领悟的代码整洁之道。

元编程抽象代码,我很早就在使用的奇技淫巧。现在却用的越来越少了,因为它违背了OOP,可维护性比较差,对使用者的水平有很大要求,也容易坑队友

简单地说,我代码中的if/else越来越少了,类越来越多了,改动起来越来方便了,改动影响的部分越来少了,美滋滋。

结语

用一句古老的名言,软件开发没有银弹。

kafka introduce翻译

年前看过一遍kafka的文档,没想到现在忘得差不多了,于是结合自己的理解来写一篇译文。

简介

kafka是一个分布式流处理平台

有三个关键特性

  • 发布、订阅,类似消息队列
  • 错误容忍的持久化流数据
  • 处理流式数据

kafka有两个比较大的应用场景

  • 构建实时流数据管道(偏向于数据传输)
  • 构建实时流数据处理(根据数据做出对应的行为)

概念

  • kafka以集群运行,支持跨dc部署
  • kafka集群分类存储流数据,叫做topic
  • 每条记录都包含key、value、timestamp

kafaka有四大核心API

  • Producer
  • Consumer
  • Stream(流数据处理)
  • Connector(和现有系统的集成)

Topics and Logs

一个topic包含多个partition,partition内部的数据是有序并且不可变的。向kafka写入数据,其实就是在向某个partition追加数据。kafka只保证单个partition内的数据是有序的!

而消费时,客户端其实就是在控制partition内部的指针。

kafka会在一个可配置的周期内,保存所有消息(无论有没有被消费)

Distribution

topic内的partitions会分布在多台server上,但partition本身不可再分。为了防止partition挂了丢失数据,partition本身也会有主从备份机制,leader处理所有读写,follower复制leader作为备份。

在宏观上,每台物理服务器会包含多个不同topic的partition,其中一部分是leader partition,一部分是follower partition,通过这样来均衡集群内的负载

Geo-Replication

提供了跨dc的复制机制,我觉得99%的企业用不上这个特性。

Producers

负责向topic发送数据,它需要通过一些策略来选择topic内的某个partition进行写入,

Consumers

kafka里的consumer有点特殊,它们会组成一个叫consumer group的概念,投递给topic的消息,最终会被订阅这个topic的consumer group中的某一个consumer消费。可以理解成,消息会被传递给consumer group,然后group内部通过某种策略选择具体的consumer处理。consumer可以分布于不同的进程或者机器上。

如果订阅某个topic的所有consumer都属于同一个group,那么消息会被高效地负载均衡。

如果订阅某个topic的所有consumer都属于不同的组,那么消息会被广播给所有的consumer

Guarantees

kafka提供的保证有三点

  • 同一个producer向同一个partition发送的数据是有序的
  • consumer看到的数据是有序的(即数据被存储的顺序)
  • topic有N个备份,能保证在N-1个server当机时不丢失数据。

优势

传统消息队列的queue和pub\sub模型,都有不足之处。

queue不支持多个subsriber,某个subsriber读了数据,这个数据就没了

pub\sub无法扩展处理能力,因为每个消息都会投递给所有subsriber

kafka以consumer group作为订阅的单位,就解决了这两个模型的不足。

kafka还是可靠又快速的存储系统,性能几乎不受数据量的影响。

参考

https://kafka.apache.org/intro#intro_multi-tenancy

使用ELK构建分布式日志分析系统

分布式系统的日志散落在各个服务器上,对于监控和排错非常不利,我们基于ELK构建了整套日志收集,分析,展示系统。

架构图

主要思路

1.整理Rails日志

我们最关心的是Rails的访问日志,但是Rails日志本身的格式是有问题的,举个例子

1
2
3
4
5
Started GET "/" for 10.1.1.11 at 2017-07-19 17:21:43 +0800
Cannot render console from 10.1.1.11! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Rails::WelcomeController#index as HTML
Rendering /home/vagrant/.rvm/gems/ruby-2.4.0@community-2.4/gems/railties-5.1.2/lib/rails/templates/rails/welcome/index.html.erb
Rendered /home/vagrant/.rvm/gems/ruby-2.4.0@community-2.4/gems/railties-5.1.2/lib/rails/templates/rails/welcome/index.html.erb (2.5ms) Completed 200 OK in 184ms (Views: 10.9ms)

可以看到,一次请求的日志散落在多行中,而且在并发情况下,不同请求的日志会交织在一起,针对这个问题,我们使用logstasher重新生成一份JSON格式的日志

1
2
{"identifier":"/home/vagrant/.rvm/gems/ruby-2.4.0@community-2.4/gems/railties-5.1.2/lib/rails/templates/rails/welcome/index.html.erb","layout":null,"name":"render_template.action_view","transaction_id":"35c707dd9d4cd1a79f37","duration":2.34,"request_id":"bc291df8-8681-47d3-8e10-bd5d93a021a0","source":"unknown","tags":[],"@timestamp":"2017-07-19T09:29:05.969Z","@version":"1"}
{"method":"GET","path":"/","format":"html","controller":"rails/welcome","action":"index","status":200,"duration":146.71,"view":5.5,"ip":"10.1.1.11","route":"rails/welcome#index","request_id":"bc291df8-8681-47d3-8e10-bd5d93a021a0","source":"unknown","tags":["request"],"@timestamp":"2017-07-19T09:29:05.970Z","@version":"1"}

2.使用Logstash收集日志

Logstash通过一份配置文件描述了数据从哪里来,经过怎样的处理流程,输出到何处这整套流程,分别对应于input,filter,output三个概念。

我们先使用简单的配置来验证一下正确性

1
2
3
4
5
6
7
8
9
10
input {
file {
path => "/home/vagrant/blog/log/logstash_development.log"
start_position => beginning
ignore_older => 0
}
}
output {
stdout {}
}

在这份配置中,我们从上一步生成的日志文件中读取,并输出到stdout中,结果如下

1
2017-07-19T09:59:01.520Z precise64 {"method":"GET","path":"/","format":"html","controller":"rails/welcome","action":"index","status":200,"duration":4.85,"view":3.28,"ip":"10.1.1.11","route":"rails/welcome#index","request_id":"27b8e5a5-dd1d-4957-9c91-435347d50888","source":"unknown","tags":["request"],"@timestamp":"2017-07-19T09:59:01.030Z","@version":"1"}

然后,修改Logstash的配置文件,将output改为Elasticsearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input {
file {
path => "/vagrant/blog/log/logstash_development.log"
start_position => beginning
ignore_older => 0
}
}

output {
elasticsearch {
hosts => [ "localhost:9200" ]
user => 'xxx'
password => 'xxx'
}
}

可以看到,整个配置文件的可读性是非常高的,input中描述了输入源是我们整理好的日志文件,输出到Elasticsearch中。

然后就可以使用Kibanana来进行日志分析的工作了。

3. Kibana的一些实践

基于Kibana,我们可以定制Elasticsearch的搜索,来查询一些非常有价值的数据

  • 查询某个接口的请求情况
  • 查询耗时在500ms以上的超慢接口
  • 查询线上报500的接口
  • 统计高频接口
    ……

4.Future

有了ELK提供的数据,我们已经可以比较方便的完成分布式情况下的错误排查,高频接口统计,为下一步的优化提供了指导。我们不必再根据业务逻辑去猜测哪些才是20%的热点,而是有了实实在在的数据支撑。

5. 问题

当然,在使用过程中也遇到过一些问题。在活动期间,访问量暴增的情况下,Elasticsearch吃了很多内存,直接拖垮了两台机器。我们通过临时关闭几台web server上的logstash暂时解决了这个问题。后续还需要对JVM进行一些调优。

自制简易前端MVC框架

周末花了大概7小时写了一个简易的响应式blog,原意是练习css的,写着写着却去实现了一套前端路由并渲染的东西,这里写一点心得体会

基本思路与涉及技术

  1. 使用url hash进行路由跳转
  2. js监听hashchange事件,根据当前hash去决定界面如何渲染
  3. 调用 addHandler(hash, func) 这个api来映射hash与handler
  4. gulp,scss, es6,模板引擎
  5. 需要一些es6的知识,需要理解this
  6. 整个工程在 https://github.com/MoonShining/front-end-hero/tree/master/blog, front-end-hero是我自己写的模板代码生成器,用它来练习CSS, 使用ruby create.rb -n name -s url来快速创建目录结构,免去重复的工作

例子

web.gif

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(()=>{

let blog = new Blog()
// add simple router
blog.addHandler(['', '#programming'], function(){
let data = {articles: [{title: 'stories to be continue', date: '2017-04-09'}]}
this.compile('#article-template', data)
})

blog.addHandler('#about', function(){
let data = {avatar: 'http://7xqlni.com1.z0.glb.clouddn.com/IMG_0337.JPG?imageView2/1/w/100/h/100', name: 'Jack Zhou'}
this.compile('#about-template', data)
})

// initialize the page
blog.init()

})()

调用blog.addHandler来自定义路由改变之后触发的动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Blog {

constructor(){
this.content = '#content'
this.router = {}
}

init(){
this.dispatch()
$(window).on('hashchange',()=>{
this.dispatch()
});
}

dispatch(){
this.handle(window.location.hash)
}

addHandler(hash, func){
if(Array.isArray(hash)){
for(let item of hash){
this.router[item] = func
}
}else{
this.router[hash] = func
}
}

handle(hash){
if(this.routeMissing(hash)){
this.handle404()
return
}
this.router[hash].call(this)
}

routeMissing(hash){
if(this.router[hash])
return false
else
return true
}

handle404(){
console.log('handler not found for this hash')
}

compile(templateSelector, data, element=this.content){
let source = $(templateSelector).html()
let template = Handlebars.compile(source)
$(element).html(template(data))
}

}

this.router是个是核心,其实也参考了一点Rails的设计,通过一个对象去保存 路由=》动作 的关系, 并且把核心逻辑都封装在Blog这个类中。

关于Netty的一些理解、实践与陷阱

核心概念的理解

Netty对于网络层进行了自己的抽象,用Channel表示连接,读写就是Channel上发生的事件,ChannelHandler用来处理这些事件,ChannelPipeline基于unix哲学提供了一种优雅的组织ChannelHandler的方式,用管道解耦不同层面的处理。现在回过头来看看,真的是非常天才和优雅的设计,是我心中API设计的典范之一了。

TCP半包、粘包

使用Netty内置的LineBasedFrameDecoder或者LengthFieldBasedFrameDecoder ,我们只要在pipeline中添加,就解决了这个问题。

Writtable问题

有时候,由于TCP的send buffer满了,向channel的写入会失败。我们需要检查channel().isWritable()标记来确定是否执行写入。

处理耗时任务

Netty In Action以及网上的一些资料中,都没有很直接的展示如何在Netty中去处理耗时任务。其实也很简单,只要给handler指定一个事件循环就可以,例如

1
2
3
4
5
6
7
8
9
10
public class MyChannelInitializer extends ChannelInitializer<Channel> {
private static EventExecutorGroup longTaskGroup = new DefaultEventExecutorGroup(5);

protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
...
pipeline.addLast(longTaskGroup, new PrintHandler());

}
}

Pitfall

Netty的ChannelPipeline只有一条双向链,消息入站,经过一串InBoundHandler之后,以相反的顺序再经过OutBoundHandler出站.因此,我们自定义的handler一般会处于pipeline的末尾!

举个例子,当以如下顺序添加handler时,如果调用ChannelHandlerContext上的writeAndFlush方法,出站消息是无法经过StringEncoder的

1
2
3
4
5
6
7
8
9
10
11
public class MyChannelInitializer extends ChannelInitializer<Channel> {
private static EventExecutorGroup longTaskGroup = new DefaultEventExecutorGroup(5);

protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(longTaskGroup, new PrintHandler());
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
}
}

这个问题有两个解决方式

  1. 调整handler的顺序
  2. 调用channel上的writeAndFlush方法,强制使消息在整个pipeline上流动

调整handler的顺序

1
2
3
4
5
6
7
8
9
10
11
public class MyChannelInitializer extends ChannelInitializer<Channel> {
private static EventExecutorGroup longTaskGroup = new DefaultEventExecutorGroup(5);

protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(longTaskGroup, new PrintHandler());
}
}

调用Channel上的writeAndFlush方法

1
2
3
4
5
6
7
public class PrintHandler extends SimpleChannelInboundHandler<String> {
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// ctx.writeAndFlush(msg);
ctx.channel().writeAndFlush(msg);
System.out.println(msg);
}
}

参考

http://www.voidcn.com/article/p-yhpuvvkx-mm.html
https://stackoverflow.com/questions/37474482/dealing-with-long-time-task-such-as-sql-query-in-netty
《Netty In Action》

一次完整的HTTP请求

这里讲的请求是后端DevOps可以控制的范围内,不包括DNS解析,层层的路由等等,一切都从请求到达我们自己架设的服务器开始。

1.与服务器建立连接

1.1 TCP连接的建立

客户端的请求到达服务器,首先就是建立TCP连接

  1. Client首先发送一个连接试探,ACK=0 表示确认号无效,SYN = 1 表示这是一个连接请求或连接接受报文,同时表示这个数据报不能携带数据,seq = x 表示Client自己的初始序号(seq = 0 就代表这是第0号包),这时候Client进入syn_sent状态,表示客户端等待服务器的回复

  2. Server监听到连接请求报文后,如同意建立连接,则向Client发送确认。TCP报文首部中的SYN 和 ACK都置1 ,ack = x + 1表示期望收到对方下一个报文段的第一个数据字节序号是x+1,同时表明x为止的所有数据都已正确收到(ack=1其实是ack=0+1,也就是期望客户端的第1个包),seq = y 表示Server 自己的初始序号(seq=0就代表这是服务器这边发出的第0号包)。这时服务器进入syn_rcvd,表示服务器已经收到Client的连接请求,等待client的确认。

  3. Client收到确认后还需再次发送确认,同时携带要发送给Server的数据。ACK 置1 表示确认号ack= y + 1 有效(代表期望收到服务器的第1个包),Client自己的序号seq= x + 1(表示这就是我的第1个包,相对于第0个包来说的),一旦收到Client的确认之后,这个TCP连接就进入Established状态,就可以发起http请求了。

1.2 常见TCP连接限制

1.2.1 修改用户进程可打开文件数限制

在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时,最高的并发数量都要受到系统对用户单一进程同时可打开文件数量的限制(这是因为系统为每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄)。可使用ulimit命令查看系统允许当前用户进程打开的文件数限制,windows上是256,linux是1024,这个博客的服务器是65535

1.2.2 修改网络内核对TCP连接的有关限制

在Linux上编写支持高并发TCP连接的客户端通讯处理程序时,有时会发现尽管已经解除了系统对用户同时打开文件数的限制,但仍会出现并发TCP连接数增加到一定数量时,再也无法成功建立新的TCP连接的现象。出现这种现在的原因有多种。
第一种原因可能是因为Linux网络内核对本地端口号范围有限制。此时,进一步分析为什么无法建立TCP连接,会发现问题出在connect()调用返回失败,查看系统错误提示消息是“Can’t assign requestedaddress”。同时,如果在此时用tcpdump工具监视网络,会发现根本没有TCP连接时客户端发SYN包的网络流量。这些情况说明问题在于本地Linux系统内核中有限制。

其实,问题的根本原因在于Linux内核的TCP/IP协议实现模块对系统中所有的客户端TCP连接对应的本地端口号的范围进行了限制(例如,内核限制本地端口号的范围为1024~32768之间)。当系统中某一时刻同时存在太多的TCP客户端连接时,由于每个TCP客户端连接都要占用一个唯一的本地端口号(此端口号在系统的本地端口号范围限制中),如果现有的TCP客户端连接已将所有的本地端口号占满,则此时就无法为新的TCP客户端连接分配一个本地端口号了,因此系统会在这种情况下在connect()调用中返回失败,并将错误提示消息设为“Can’t assignrequested address”。

2.发起HTTP请求

2.1 请求格式

例如这样的一个请求

Accept 就是告诉服务器端,我接受那些MIME类型

Accept-Encoding 这个看起来是接受那些压缩方式的文件

Accept-Lanague 告诉服务器能够发送哪些语言

Connection 告诉服务器支持keep-alive特性

Cookie 每次请求时都会携带上Cookie以方便服务器端识别是否是同一个客户端

Host 用来标识请求服务器上的那个虚拟主机,比如Nginx里面可以定义很多个虚拟主机
那这里就是用来标识要访问那个虚拟主机。

User-Agent 用户代理,一般情况是浏览器,也有其他类型,如:wget curl 搜索引擎的蜘蛛等

条件请求首部:
If-Modified-Since 是浏览器向服务器端询问某个资源文件如果自从什么时间修改过,那么重新发给我,这样就保证服务器端资源
文件更新时,浏览器再次去请求,而不是使用缓存中的文件

安全请求首部:
Authorization: 客户端提供给服务器的认证信息

2.2 keep-alive/persitent

每次HTTP请求都重新建立TCP连接的开销是很大的,于是就出现了keep-alive这个首部,它允许在一次TCP连接中发送/接收多个HTTP报文

然而,keep-alive是有弊端的。在HTTP1.0中,客户端发起请求是加上keep-alive首部,服务端响应时也加上keep-alive首部,那么这个请求就被认为是keep-alive的,直到其中一方主动断开为止。如果没有正确断开,这个资源就会一直被占用了。

哑代理问题:哑代理只是单纯的转发请求,并不能进行解析处理、维持持久连接等其他工作,而聪明的代理可以解析接收到的报文同时可以维持持久连接。

 如上图,当客户端与服务器之间存在不解析直接转发的代理时,connection:keep-alive这个首部是直接转发给服务器的,服务器接收了这个请求之后,就会向客户端发送带有connection:keep-alive的响应,同样盲代理不会解析响应,直接将全部响应转发回客户端。因为客户端收到了这个首部,就认为建立持久连接已经成功了,但是中间的”笨代理“,并不知道这些事情,笨代理只有一种行为模式:在转发请求和回送服务器响应请求之后就认为这次事务结束了,等待连接断开,而这时由于connection:keep-alive首部已经发送到服务器和客户端,双方都认为持久连接已经建立完成,这样就变成了两边认为持久连接OK而中间的哑代理等待连接断开的情况,这种情况下如果客户端再一次在这条连接上发送请求,请求就会在亚代理处停止,因为哑代理已经在等待连接关闭。这种状态会导致浏览器一直处于挂起状态,直到客户端或服务器之中一个连接超时,关闭连接为止,一段美好的牵手就这么没了(哑代理就是把内容原封不动的转发到代理)。

为了避免这种情况,现代的代理是不会转发connection:keep-alive这个首部的。

persistent

HTTP/1.1的持久连接默认是开启的,只有首部中包含connection:close,才会事务结束之后关闭连接。当然服务器和客户端仍可以随时关闭持久连接。

当发送了connection:close首部之后客户端就没有办法在那条连接上发送更多的请求了。当然根据持久连接的特性,一定要传输正确的content-length。

还有根据HTTP/1.1的特性,是不应该和HTTP/1.0客户端建立持久连接的。最后,一定要做好重发的准备。

管道化连接

HTTP/1.1允许在持久连接上使用管道,这样就不用等待前一个请求的响应,直接在管道上发送第二个请求,在高延迟下,提高性能。

管道化连接的限制:

  • 不是持久连接就不能使用管道。
  • 必须按照同样的发送顺序回送响应,因为报文没有标签,很可能就顺序就乱咯。
  • 因为可以随时关闭持久连接,所以要随时做好重发准备
  • 不应该使用管道化发送重复发送会有副作用的请求(如post,重复提交)。

3.负载均衡

接收到HTTP请求之后,就轮到负载均衡登场了,它位于网站的最前端,把短时间内较高的访问量分摊到不同机器上处理。负载均衡方案有软件、硬件两种

F5 BIG-IP是著名的硬件方案,但这里不作讨论

软件方案有LVS HAProxy Nginx等,留作以后补充

4.Nginx(WEB服务器)

在典型的Rails应用部署方案中,Nginx的作用有两个

  1. 处理静态文件请求
  2. 转发请求给后端的Rails应用

这是一个简单的Nginx配置文件

后端的Rails服务器通过unix socket与Nginx通信,Nginx伺服public文件夹里的静态文件给用户

5.Rails(应用服务器)

这篇文章无敌了,我没有更多可以写的,只能说一句我用的是Puma。因为服务器是单核的,只能用多线程Puma或者事件驱动的Thin,考虑到以后可能用上Rails 5 ActionCabel,还是直接上Puma吧。

6.数据库(数据库服务器)

应用服务器想访问数据库,就需要与数据库建立连接。Rails读取database.yml中的配置,访问对应的数据库。

一个重要的配置指标pool: Rails中的数据库连接是完全线程安全的,所有pool的值要配置成与Puma的最大线程数相等,这样就不会出现线程等待数据库连接的情况。

7.Redis、Memercache(缓存服务器)

8.消息队列

9.搜索

###参考文献
一次完整的HTTP事务是怎样一个过程?

Linux下高并发socket最大连接数所受的各种限制

Keep-Alive

谈谈持久连接——HTTP权威指南读书心得(五)

数据库连接池的工作原理

MySQL查询优化

2017.12.11更新
The Secret Life of SQL: How to Optimize Database Performance by Bryana Knight

查询性能低下的原因是访问了太多的数据

  • 多表连接时返回了所有的列
1
2
3
4
select * from sakila.actor 
inner join sakila.file_actor using(actior_id)
inner join sakila.film using(film_id)
where sakila.film.title = 'AronMan'
正确的做法是这样
1
2
3
4
select sakila.actor.* from sakila.actor 
inner join sakila.file_actor using(actior_id)
inner join sakila.film using(film_id)
where sakila.film.title = 'AronMan'
  • 分解连接技术
1
2
3
4
select * from tag
join tag_post on tag_post.tag_id=tag.id
join post on tag_post.post_id=post.id
where tag.tag='mysql'

分解连接之后

1
2
3
select * from tag where tag='mysql'
select * from tag_post where tag_id=1234
select * from post where post.id in(123,456,789)

分解连接看上去比较浪费,但是有巨大优势

  1. 缓存效率高
  2. MyISAM引擎下,锁住表的时间短
  3. 在应用程序端连接可以更方便扩展数据库,把表放在不同的数据库服务器上
  4. 查询本身更高效
  5. 减少多余行的访问

什么时候使用分解连接?

  1. 可以缓存大量查询
  2. 使用了多个MyISAM表
  3. 数据分布在不同服务器
  4. 对于大表使用in替换连接
  5. 一个连接引用了同一个表多次

优化连接

  1. 确保on或者using的列有索引
  2. 确保group by 或者order by只引用一个列,这样可以使用索引

悲观锁

1
2
3
select chairid from seat where booked is null for update
update seat set booked='x' where chairid=1
commit

索引及查询优化

摘取部分自mysql性能优化-慢查询分析、优化索引和配置

索引的类型

Ø 普通索引:这是最基本的索引类型,没唯一性之类的限制。

Ø 唯一性索引:和普通索引基本相同,但所有的索引列值保持唯一性。

Ø 主键:主键是一种唯一索引,但必须指定为”PRIMARY KEY”。

Ø 全文索引:MYSQL从3.23.23开始支持全文索引和全文检索。在MYSQL中,全文索引的索引类型为FULLTEXT。全文索引可以在VARCHAR或者TEXT类型的列上创建。

使用多列索引 要注意最左前缀问题

有时MySQL不使用索引,即使有可用的索引。一种情形是当优化器估计到使用索引将需要MySQL访问表中的大部分行时。(在这种情况下,表扫描可能会更快些)。然而,如果此类查询使用LIMIT只搜索部分行,MySQL则使用索引,因为它可以更快地找到几行并在结果中返回。

合理的建立索引的建议:

(1) 越小的数据类型通常更好:越小的数据类型通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快。

(2) 简单的数据类型更好:整型数据比起字符,处理开销更小,因为字符串的比较更复杂。在MySQL中,应该用内置的日期和时间数据类型,而不是用字符串来存储时间;以及用整型数据类型存储IP地址。

(3) 尽量避免NULL:应该指定列为NOT NULL,除非你想存储NULL。在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值

这部分是关于索引和写SQL语句时应当注意的一些琐碎建议和注意点。

  1. 当结果集只有一行数据时使用LIMIT 1

  2. 避免SELECT *,始终指定你需要的列

从表中读取越多的数据,查询会变得更慢。他增加了磁盘需要操作的时间,还是在数据库服务器与WEB服务器是独立分开的情况下。你将会经历非常漫长的网络延迟,仅仅是因为数据不必要的在服务器之间传输。

  1. 使用连接(JOIN)来代替子查询(Sub-Queries)。 连接(JOIN)之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。

  2. 使用ENUM、CHAR 而不是VARCHAR,使用合理的字段属性长度

  3. 尽可能的使用NOT NULL

  4. 固定长度的表会更快

  5. 拆分大的DELETE 或INSERT 语句

  6. 查询的列越小越快

Where条件

在查询中,WHERE条件也是一个比较重要的因素,尽量少并且是合理的where条件是很重要的,尽量在多个条件的时候,把会提取尽量少数据量的条件放在前面,减少后一个where条件的查询时间。

有些where条件会导致索引无效:

Ø where子句的查询条件里有!=,MySQL将无法使用索引。

Ø where子句使用了Mysql函数的时候,索引将无效,比如:select * from tb where left(name, 4) = ‘xxx’

Ø 使用LIKE进行搜索匹配的时候,这样索引是有效的:select * from tbl1 where name like ‘xxx%’,而like ‘%xxx%’ 时索引无效

技巧整理

1、应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

2、对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

3、应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:

1
select id from t where num is null

可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:

1
select id from t where num=0

4、尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:

1
select id from t where num=10 or num=20

可以这样查询:

1
2
3
select id from t where num=10
union all
select id from t where num=20

5、下面的查询也将导致全表扫描:(不能前置百分号)

1
select id from t where name like '%abc'

若要提高效率,可以考虑全文检索。

6、in 和 not in 也要慎用,否则会导致全表扫描,如:

1
select id from t where num in(1,2,3)

对于连续的数值,能用 between 就不要用 in 了:

1
select id from t where num between 1 and 3

7、如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:

1
select id from t where num=@num

可以改为强制查询使用索引:

1
select id from t with(index(索引名)) where num=@num

8、应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:

1
select id from t where num/2=100

应改为:

1
select id from t where num=100*2

9、应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:

1
select id from t where substring(name,1,3)=’abc’

应改为:

1
2
select id from t where name like ‘abc%’
select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′

10、不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

11、在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使 用,并且应尽可能的让字段顺序与索引顺序相一致。

12、不要写一些没有意义的查询,如需要生成一个空表结构:

1
select col1,col2 into #t from t where 1=0

这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:

1
create table #t(…)

13、很多时候用 exists 代替 in 是一个好的选择:

1
select num from a where num in(select num from b)

用下面的语句替换:

1
select num from a where exists(select 1 from b where num=a.num)

14、并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段 sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

15、索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有 必要。

16.应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。

17、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会 逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

18、尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

19、任何地方都不要使用 select from t ,用具体的字段列表代替“”,不要返回用不到的任何字段。

20、尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。

21、避免频繁创建和删除临时表,以减少系统表资源的消耗。

22、临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使 用导出表。

23、在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

24、如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

25、尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

26、使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

27、与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

28、在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。

29、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

30、尽量避免大事务操作,提高系统并发能力。

使用mina自动化部署Rails应用

为什么需要自动化部署?

因为我懒啊!!!

简介

mina是rails社区中流行的部署方案,允许你用Ruby DSL描述部署过程,mina生成一份脚本在目标服务器上执行。除了Ruby以外,也支持shell脚本,所以可定制性非常高。相比于老牌的Capstrino,mina一次性上传所有脚本到目标服务器执行,效率上会高一些。

核心步骤与原理

mina setup生成目标服务器上的文件夹结构

mina deploy进行部署

mina会自动从设置好的git仓库拉代码,运行bundle/migration等一系列流程,然后重启服务器

简单对比

手动部署

  1. git pull

  2. bundle install

  3. rake db:migrate

  4. rake assets:precompile

  5. restart web server

自动部署

  1. mina deploy

一条命令就可以跑完整个发布流程。当然前提是你的deploy task写的天衣无缝,这需要对rails与linux有一定了解才可以做到。

举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

require 'mina/bundler'

require 'mina/rails'

require 'mina/git'

require 'mina/rvm'

set :domain, '121.42.12.xx' #你的服务器地址或域名

set :deploy_to, '/var/www/api' #你打算把项目部署在服务器的哪个文件夹

set :repository, 'git@github.com:xxx.git' #git仓库

set :branch, 'master' #git分支

set :term_mode, nil #mina的小bug,设为nil可以解决

set :shared_paths, ['config/sidekiq.yml', 'config/database.yml', 'config/secrets.yml', 'log', 'shared'] #很关键,这几个文件夹会在多次部署间,通过符号链接的形式共享

set :user, 'moon' #ssh 用户名

task :environment do

invoke :'rvm:use[ruby-2.1.0-p0@default]' #ruby 版本

end

task :setup => :environment do #初始化task,创建文件夹结构

queue! %[mkdir -p "#{deploy_to}/#{shared_path}/log"]

queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/log"]

queue! %[mkdir -p "#{deploy_to}/#{shared_path}/config"]

queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/config"]

queue! %[mkdir -p "#{deploy_to}/#{shared_path}/shared"]

queue! %[chmod g+rx,u+rwx "#{deploy_to}/#{shared_path}/shared"]

queue! %[touch "#{deploy_to}/#{shared_path}/config/database.yml"]

queue! %[touch "#{deploy_to}/#{shared_path}/config/secrets.yml"]

queue %[echo "-----> Be sure to edit '#{deploy_to}/#{shared_path}/config/database.yml' and 'secrets.yml'."]

if repository

repo_host = repository.split(%r{@|://}).last.split(%r{:|\/}).first

repo_port = /:([0-9]+)/.match(repository) && /:([0-9]+)/.match(repository)[1] || '22'

queue %[

if ! ssh-keygen -H -F #{repo_host} &>/dev/null; then

ssh-keyscan -t rsa -p #{repo_port} -H #{repo_host} >> ~/.ssh/known_hosts

fi

]

end

end

desc "Deploys the current version to the server."

task :deploy => :environment do

to :before_hook do

end

deploy do #部署流程

invoke :'git:clone'

invoke :'deploy:link_shared_paths'

invoke :'bundle:install'

invoke :'rails:db_migrate'

#invoke :'rails:assets_precompile'

invoke :'deploy:cleanup'

invoke :start

to :launch do

queue "mkdir -p #{deploy_to}/#{current_path}/tmp/"

queue "touch #{deploy_to}/#{current_path}/tmp/restart.txt"

end

end

end

desc "start puma & sidekiq"

task :start => :environment do

queue "cd #{deploy_to+"/current"}"

queue "sidekiq -e production -d"

queue "puma -e production"

end