分类 技术 下的文章

写在前面

Go中的错误处理与Java、JavaScript或Python等其他主流编程语言略有不同。Go的内置错误不包含堆栈跟踪,也不支持传统的尝试/捕获方法来处理它们。相反,Go中的错误只是函数返回的值,可以用与任何其他数据类型几乎相同的方式来处理它们,这导致了一个惊人的轻量级和简单的设计。

在本文中,我将演示Go中处理错误的基础知识,以及一些可以在代码中遵循的简单策略,以确保程序健壮且易于调试。

error关键字

在golang中error关键字是一个interface,他的定义如下:

type error interface {
    Error() string
}

因此任何实现了interface的类型,都可以有Error() 的表现,它会返回一段字符串,也正是这样简介的表达,让golang中的错误处理异与任何其他编程语言。

实现接口

在golang标准库中,总能让我看到特别优化的设计,就比如如何实现一个错误,官方标准库如下:

- 阅读剩余部分 -

背景

由于业务需要,我们在生产环境下利用rabbitmq作为消息通信的服务, 今天发现一个奇怪的现象, 服务端存在大量的Connections,同时有同等多的channels, 但是根据业务上分析, 我们完全用不到这么多的连接, 随后我们进行了排查问题的第一步:

初步思路

打开client所在的服务器

查看连接5672(amqp端口)的tcp数量以及状态

 ss -aoen state established | grep 5672
  ss -aoen state established | grep 5672 | wc -l

这个只是部分截图, 但是通过ss分析,可以肯定两件事情:

  1. 客户端维护的5672 tcp数量是很少的, 也是符合业务场景的
  2. 每一个tcp都维护了正常的keepalive

打开rabbitmq-server所在的服务器

查看连接5672(amqp端口)的tcp数量以及状态

 ss -aoen state established | grep 5672
 ss -aoen state established | grep 5672 | wc -l

通过分析发现:

  1. 服务端tcp数量很大
  2. 服务端tcp没有keepalive

进一步猜想

服务端存活着大量的dead connection, 由于客户端没有正确的关闭, 正常情况下客户端即使没有正常断开连接, 但是应该由服务端心跳检测, 或者借助tcp keepalive来实现服务端断开dead connection.但是这两种方式均为生效

由此有如下猜想:

1.服务端(rabbitmq-server)没有开启tcp keepalive?

  1. 客户端没有配置心跳?

证实猜想

翻阅官方文档, 阅读后, 可以总结为:

作为amqp协议, 鼓励你使用心跳来做conn的维护,客户端 捕获心跳异常来重连, 如果正确配置了心跳,那么服务端在心跳实际发现客户端无响应, 自然会断开.

随后, 去查看php amqp的源码, 发现如下:

    public function __construct(
        $host,
        $port,
        $user,
        $password,
        $vhost = '/',
        $insist = false,
        $login_method = 'AMQPLAIN',
        $login_response = null,
        $locale = 'en_US',
        $connection_timeout = 3.0,
        $read_write_timeout = 3.0,
        $context = null,
        $keepalive = false,
        $heartbeat = 0,
        $channel_rpc_timeout = 0.0,
        $ssl_protocol = null

if ($this->keepalive) {
    $this->enable_keepalive();
}
    protected function enable_keepalive()
    {
        if ($this->protocol === 'ssl') {
            throw new AMQPIOException('Can not enable keepalive: ssl connection does not support keepalive (#70939)');
        }
        .......
        .......

        $socket = socket_import_stream($this->sock);
        socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
    }

由上面的源码可以分析到, php的这个lib, 在默认情况下, 完全禁用了heartbeats以及keepalive, 由此证明猜想.

A zero value indicates that a peer suggests disabling heartbeats entirely. To disable heartbeats, both peers have to opt in and use the value of 0. This is highly recommended against unless the environment is known to use TCP keepalives on every host.

官方文档中明确说明, 除非特殊情况, 否则强烈建议使用心跳.

遗留的疑问

  1. tcp keepalive 还需要服务端应用程序额外开启?

根据php的源码socket_set_option中可以看到需要客户端明确开启keepalive,底层调用 ` socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
` 实现.

额外补充一个golang版本的代码:

这是客户端:

func main() {
    var tcpAddr *net.TCPAddr
    tcpAddr,_ = net.ResolveTCPAddr("tcp","127.0.0.1:9999")

    conn,err := net.DialTCP("tcp",nil,tcpAddr)
    // 这里设置和不设置影响客户端tcp keepalive
    conn.SetKeepAlive(true)

通过ss查看:

上面是未设置SetKeepAlive, 由此可以看出, 客户端需要明确指定keepalive来使用tcp keepalive机制

总结

整体写了心跳以及tcp keepalive的问题, 也发现了线上php amqplib的默认值导致的该问题, 所以这里修改php的初始化方法, 给心跳分配一个时间(golang版本默认值是15s)

tips

这里涉及到后续要删掉那些死连接, 这里提供一个小小的tips

rabbitmqctl list_connections pid port state user recv_cnt send_cnt send_pend name client_properties | more

这里可以获取client属性,然后匹配php版本,全量关闭,然后重连即可.

Too many open files 是提供http服务的应用程序很容易遇到的问题, 通常是由于系统配置不当或者程序本身打开太多文件导致的, 于是网上有两种提示你解决该类问题的办法.

  1. 你的系统配置的最大文件打开数太低了
  2. 你的程序df泄露了

本篇内容主要围绕第一个原因展开描述,问题先从这个issue开始说起

这里有人回复:

Check your hard and soft limits for file descriptors (respectively ulimit -Hn and ulimit -Hs) and your (linux) kernel settings (cat /pro/sys/fs/file-max) as these may need to be adjusted.

这里提到3个概念

  1. ulimit -Hn
  2. ulimit -Hs
  3. /pro/sys/fs/file-max

带着这3个指标, 我们开始查阅资料

什么是ulimit

help ulimit # 注意这里的命令,请切换到bash上操作,而不是zsh

修改 shell 资源限制
在允许此类控制的系统上,提供对于 shell 及其创建的进程所可用的资源的控制。

由此,我们可以看出, 我们跑在linux上的大部分程序, 都是有shell所创建进程, 那么ulimit就如同一个配置文件, 限制了我们使用系统资源.

如果你使用help ulimit, 可以查看到如下参数配置项

选项:
  -S    使用 `soft'(软)资源限制
  -H    使用 `hard'(硬)资源限制
  -a    所有当前限制都被报告
  -b    套接字缓存尺寸
  -c    创建的核文件的最大尺寸
  -d    一个进程的数据区的最大尺寸
  -e    最高的调度优先级(`nice')
  -f    有 shell 及其子进程可以写的最大文件尺寸
  -i    最多的可以挂起的信号数
  -l    一个进程可以锁定的最大内存尺寸
  -m    最大的内存进驻尺寸
  -n    最多的打开的文件描述符个数
  -p    管道缓冲区尺寸
  -q    POSIX 信息队列的最大字节数
  -r    实时调度的最大优先级
  -s    最大栈尺寸
  -t    最大的CPU时间,以秒为单位
  -u    最大用户进程数
  -v    虚拟内存尺寸
  -x    最大的锁数量
  

这其中, 有我们关心的内容, 软资源和硬资源

什么是soft和hard

这里通过查阅资料整理如下:

  1. 理解soft和hard, 首先要想到是权限需求
  2. soft 必须小于hard
  3. hard严格的设定,必定不能超过这个设定的数值
  4. soft警告的设定,可以超过这个设定值,但是若超过则有警告信息
  5. 一个process可以修改当前process的soft或hard

ulimit的修改与生效

  1. ulimit的值总是继承父进程的设置。
  2. 可以利用ulimit设置当前shell进程
  3. 增加hard, 只能由root来

下面通过几个例子来践行上面的理论知识:

  1. 修改/etc/security/limits.conf
一条记录包含4️列,分别是范围domain(即生效的范围,可以是用户名、group名或*代表所有非root用户);t类型type:即soft、hard,或者-代表同时设置soft和hard;项目item,即ulimit中的资源控制项目,名字枚举可以参考文件中的注释;最后就是value。
* hard nofile 1000000 
* soft nofile 1000

Question: 修改后不重启当前shell, limits的修改是否生效
A: 否

  1. 当前shell配置ulimit
#!/bin/bash
ulimit -Sn 2045
nc -l 8888

然后ps -ef 查看nc的进程, 然后 cat /proc/{$pid}/limits

Max open files 2045 1000000 files

可以看到这里已经通过shell修改了soft nofile

修改系统最大的df数

除了ulimit控制外,/proc/sys/fs/file-max这个文件控制了系统内核可以打开的全部文件总数。所以,即便是ulimit里nofile设置为ulimited,也还是受限的。

系统级别: /proc/sys/fs/file-max

用户级别: /etc/security/limits.conf

单个进程: fs.nr_open

ulimit 常见命令

  • ulimit -a # 查看所有soft值
  • ulimit -Ha # 查看所有hard值
  • ulimit -Hn # 查看nofile的hard值
  • ulimit -Sn 1000 # 将nofile的soft值设置为1000
  • ulimit -n 1000 # 同时将nofiles的hard和soft值设置为1000

参考资料:
https://juejin.im/post/5d4cf32f6fb9a06b1d21312c
https://blog.csdn.net/liuleinevermore/article/details/83473919

love

前述

Go1.14已经发布有一阵子了,下面是我学习的过程,也是知识的整理.

下载地址

下载地址

详细说明

可查看本地安装之后的doc文件夹,里面包含了go1.14.html文件,直接在本地打开即可食用

哪些地方做了优化

工具层面

go module

1.Flags
-modfile=file,现在可以直接指定mod file了,不在是固定的项目目录下了

语言&&Runtime

聚合接口

在1.14之前,我们聚合接口的定义不能有相同签名和返回值的方法,但是1.14取消了这个限制.

type Foo interface {
    GetName() string
    SetFooName(string)
}

type Bar interface {
    GetName() string
    SetBarName() string
}

type FooBar interface {
    Foo
    Bar
}
defer零消耗

1.14之前,defer的runtime要维护堆栈,我们要压栈和出栈的性能损耗,但是现在在编译器层做了defer的优化

goroutine 基于信号的抢占式调度实现

经典的例子,设置p为1,这样规避并发,然后go协程初始化到队列等待runtime调度,然而for循环的存在,调度一直调度不到,然后现在1.14可以利用信号机制抢占for的循环资源了.

package main

import (
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(1)

    go func() {
        panic("already call")
    }()

    for {
    }
}

思考的问题:抢占是否影响性能 ?

抢占通常是由于阻塞性系统调用引起的,例如磁盘io,cgo等,所以一定程度上很难形成协程阻塞调度.

计时器优化,内存分配器

这里主要学习 这篇文章

包层面

test包
func TestIt(t *testing.T) {
    t.Cleanup(func() {
        fmt.Println("clean up 一些事情")
    })
    
    t.Log("这是个log")
    time.Sleep(time.Second * 2)
}

go1.14 会在sleep之前就输出log日志.同时增加了Cleanup方法(这个方法很有用,在我们测试完一个用例后可以做一些善后处理,例如rollback db等)

hash/maphash

降低了hash冲突的可能性.

json

InputOffset()方法

func main() {
    const jsonStr = `
        {"Job":"java", "Name":"zhangsan"}
        {"Job":"php", "Name":"lisi"},
        {"Job":"python", "Name":"wangwu"}
`
    type Jober struct {
        Job, Name string
    }

    dec := json.NewDecoder(strings.NewReader(jsonStr))
    for {
        var m Jober
        if err := dec.Decode(&m); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
        fmt.Println(m)
        fmt.Println(dec.InputOffset())
    }

}

输出:

{java zhangsan}
36
{php lisi}
67
2020/03/11 21:39:57 invalid character ',' looking for beginning of value

这里我特意后面多加了',', 可以看的出来解析的时候加入InputOffset很有用

ioutil

TempDir 第二个参数,随机字符串模式,可以是一个更加定制化的模式

    ioutil.TempDir("/tmp/ss", "name-date-*-test")

这里的 * 可以在一个字符串模式的中间

log
logger = log.New(&buf, "logger", log.Lmsgprefix)

文件名和行数放在prefix的前面

reflect包

StructOf 可以设置私有字段了

func main() {
    typ := reflect.StructOf([]reflect.StructField{
        {
            Name: "Height",
            Type: reflect.TypeOf(float64(0)),
            Tag:  `json:"height"`,
        },
        {
            Name: "age",
            Type: reflect.TypeOf(int(0)),
            Tag:  `json:"age"`,
            PkgPath: "other/path",
        },
    })

    v := reflect.New(typ).Elem()
    v.Field(0).SetFloat(0.4)
    v.Field(1).SetInt(2)
    s := v.Addr().Interface()

    w := new(bytes.Buffer)
    if err := json.NewEncoder(w).Encode(s); err != nil {
        panic(err)
    }

    fmt.Printf("value: %+v\n", s)
    fmt.Printf("json:  %s", w.Bytes())

    r := bytes.NewReader([]byte(`{"height":1.5,"age":10}`))
    if err := json.NewDecoder(r).Decode(s); err != nil {
        panic(err)
    }
    fmt.Printf("value: %+v\n", s)

}

这里的age是一个不可导出的私有属性,但是我同样可以在structof中设置

runtime

Goexit()

func todo() {
    defer func() {
        fmt.Println(recover())
    }()
    defer panic("cancelled goexit")
    runtime.Goexit()
}

func main() {
    todo()
}

这里在1.13中,由于goexit退出当前协程而报错,但是此时会被recover住,但是1.14将会顺利报错.

strconv包

strconv包中的NumError类型现在支持is判断不同的类型,代码如下:

    str := "hello world"
    if _, err := strconv.ParseFloat(str, 64); err != nil {
        if errors.Is(err, strconv.ErrSyntax) {

        }
        if errors.Is(err, strconv.ErrRange) {

        }
    }

前述

我们在写一个web模块或者api接口的时候,我们总是能够想到利用mvc模式,将model层,controller的action层分离, 使得我们的代码总是干净,可维护性强,注入新的服务简单.这一篇博客,我将探索在go中如何利用golang语言的特性,写一个干净,整洁,注入型强的代码.

一个简单的代码需求

  1. 起一个web server
  2. 实现用户注册接口
  3. 单元测试完备
  4. 后期新的需求,可以快速在现有代码的基础上完成

尽量编写testable的代码

我们总是希望,我们的testing中枚举用例不受过多服务的牵连,也不受过多业务逻辑的牵连
例如,用户有existscreate方法,那我们最好的方式则是创建一组这样的user相关的接口,大致代码如下:

type Repository interface {
    Exists(email string) bool
    Create(*Form) (*User, error)
}
func (m *Memstore) Exists(email string) bool {
    return true
}

func (m *Memstore) Create(*Form) (*User, error) {
    return &User{
        Id:       1,
        Email:    "test@qq.com",
        Password: "123",
    }, nil
}

还有我们的user

type User struct {
    Id int `json:"id"`
    Email string `json:"email"`
    Password string `json:"password"`
}

那么我们对服务的testing可以这样:

func TestMemstore_Exists(t *testing.T) {
    type fields struct {
        Users []User
    }
    type args struct {
        email string
    }
    tests := []struct {
        name   string
        fields fields
        args   args
        want   bool
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            m := &Memstore{
                Users: tt.fields.Users,
            }
            if got := m.Exists(tt.args.email); got != tt.want {
                t.Errorf("Exists() = %v, want %v", got, tt.want)
            }
        })
    }
}

实现handler

我们绑定/register 接口到handle上面, 注意这里不是handleFunc, 我们需要做的是在handle上注入更多的toolings,例如: model模型,Validater组件等

// 他需要两个服务,一个create,一个exists
type RegisterHandler struct {
    Repository
}

// 实现注册服务
func (r *RegisterHandler) ServeHTTP(http.ResponseWriter, *http.Request) {

}

这里有很重要的两点, 我们自己实现了http包的handler interface,我们将可testing的model模型绑定到了registerhandler上

我们在这里还可以绑定更多的组件

type RegistrationHandler struct {
    Validator *validator.Validate
    Repository

实现handler接口的绑定

func NewServer(addr string, r Repository) error {
    mux := http.NewServeMux()
    h := &RegisterHandler{r}
    mux.Handle("register", h)

    server := http.Server{
        Addr:    addr,
        Handler: h,
    }
    err := server.ListenAndServe()
    if err != nil {
        return err
    }
    return nil
}

这里将h注册到handle上,并将Repository具体实现传递给handler.

总结

当新的需求来临时,代码耦合的可能性增加,因为没有明显的设计来保护。服务对象的方式我们传递给每一个handler,使得每一个对象自己的独立功能尽可能不耦合,这样后期的可维护性也就大大增加了.