CharlesXiao‘s Blog

  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

  • 搜索

Mysql 长连接与短连接

发表于 2017-12-09 | 更新于 2018-01-18 | 字数统计1.3k字 | 阅读时长4分钟

概念说明

  1. 短连接:是指程序和数据库通信时需要建立连接,执行操作后,连接关闭。短连接简单来说就是每一次操作数据库,都要打开和关闭数据库连接,基本步骤是:连接→数据传输→关闭连接。
  2. 长连接:是指程序之间的连接在建立之后,就一直打开,被后续程序重用,基本步骤是:连接→数据传输→保持连接→数据传输……。使用长连接的初衷是减少连接的开销,尽管MySQL的连接比其他数据库要快得多。长连接在没有数据通信时,定时发送数据包,以维持连接状态。
  3. 数据库连接池:是一些网络代理服务或应用服务器实现的特性,如J2EE服务器,它实现了一个持久连接的“池”,允许其他程序、客户端来连接,这个连接池将被所有连接的客户端共享使用,连接池可以节省打开数据库的时间,加速连接,也可以减少数据库连接,降低数据库服务器的负载;它是预先打开N个数据库连接,把它们缓存起来,当需要使用数据库的时候就直接使用这些已经打开的连接,节省时间
  4. J2EE数据库连接池的原理:
    • J2EE服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。
    • 客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其表记为忙。
    • 如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量由配置参数决定。
    • 当使用的池连接调用完成后,池驱动程序将此连接表记为空闲,其他调用就可以使用这个连接。

适用场景

  1. 长连接优劣势
    • 从客户端的角度来说,使用长连接可以不用每次创建新连接,若客户端对MySQL服务器的连接请求很频繁,永久连接将更加高效。
    • 从服务器的角度来看,情况则略有不同,它可以节省创建连接的开销,但维持连接也是需要内存的。如果滥用长连接的话,可能会使用过多的MySQL服务器连接。现代的操作系统可以拥有几千个MySQL连接,但很有可能绝大部分都是睡眠(sleep)状态的,这样的工作方式不够高效,而且连接占据内存,也会导致内存的浪费。
  2. 长连接主要用于在少数客户端与服务端的频繁通信,因为这时候如果用短连接频繁通信常会发生Socket出错,并且频繁创建Socket连接也是对资源的浪费;但是对于服务端来说,长连接也会耗费一定的资源,需要专门的线程(unix下可以用进程管理)来负责维护连接状态。
  3. 长连接是一些驱动、驱动框架、ORM工具的特性,由驱动来保持连接句柄的打开,以便后续的数据库操作可以重用连接,从而减少数据库的连接开销。而连接池是应用服务器的组件,它可以通过参数来配置连接数、连接检测、连接的生命周期等。

注意事项

  1. 我们一般使用mysql -uroot -p只不过是使用了管理员的身份来创建一个connection,从而登录mysql,mysql的连接过程,内部实际上是经过tcp/ip协议的,当然mysql封装了tcp/ip有自己的一套协议。mysql是会创建一个线程来处理到来的连接的,我们可以在mysql中show status;然后在连接mysql,再次show status就可以看到Thread_connected的数量会增加1
  2. 在生产繁忙的系统中,连接也可能会受到系统端口数的限制,如果要每秒建立几千个连接,那么连接断开后,端口不会被马上回收利用,必须经历一个“FIN”阶段的等待,直到可被回收利用为止,这样就可能会导致端口资源不够用。
  3. 如果客户端和MySQL数据库之间有连接池或Proxy代理,一般在客户端推荐使用短连接。对于长连接的使用一定要慎重,不可滥用
  4. 如果使用了长连接而长期没有对数据库进行任何操作,那么在timeout值(默认8小时)后,mysql server就会关闭此连接,而客户端在执行查询的时候就会得到一个类似于“MySQL server has gone away“这样的错误。在使用mysql_real_connect连接数据库之后,再使用mysql_options( &mysql, MYSQL_OPT_RECONNECT, … ) 来设置为自动重连。

常用工具

  1. 查看mysql连接数:mysqladmin -uroot -p processlist

如何实现多个数据库分片的list_objects操作

发表于 2017-12-09 | 更新于 2018-03-23 | 字数统计2.5k字 | 阅读时长9分钟

业务场景

我们需要实现一个类似于Linux中ls命令的功能,用户可以用该功能来查看的bucket里边object列表,这些object信息存储在mysql meta表和shard meta表中;正常情况下一个bucket中的所有object存储在一张meta表中;然而当一个bucket中的object数量十分庞大时,我们采用了水平分表的方式,将这些object通过哈希方式分散到了1024个shard meta表中,以避免单表行数过大带来的性能问题。

因此,ls功能的实现需要考虑以上两种情景,分别是单表和多表的list;list功能需要支持delimiter来折叠文件夹(类似于linux中ls命令,根据该参数来决定是否展开当前目录下的子目录),marker来指定每次查询的起始位置,maxKeys来指定每次返回数目,prefix来筛选出以前缀开头的object。单表list我们可以直接采用mysql order来保证结果有序;如果object分散在了1024个shard meta表中,要每次拿出前n个就比较困难了,因为数据只是单表有序的,要想全局有序,就还得做一些处理。

分表hash方式

如何把很多object通过哈希方式分散到了1024个shard meta表中?如下代码所示,我们根据bucket和object构建一个url字符串,然后求MD5值,然后按4位做异或操作,最后对shard数目取模,就可以把object都随机分散到1024个表里了;MD5算法对原信息进行数学变换后得到的一个128位(bit)的特征码作为数据摘要,具有高度的离散性,原信息的一点点变化就会导致MD5的巨大变化。

void GenUrl(const std::string& bucket, const std::string& object, std::string& url) {
    url.append("bs://");
    url.append(bucket);
    url.append("/");
    url.append(object);
}

int32_t GetShardKey(const std::string &bucket, const std::string &object) {
    std::string url = "";
    GenUrl(bucket, object, url);
    std::string md5sum = MD5(url);
    const uint32_t *p = reinterpret_cast<const uint32_t*>(md5sum.data());
    return static_cast<int32_t>((p[0]^p[1]^p[2]^p[3]) % BUCKET_SHARD_NUM);
}

单表list

  • 解决方案

    • 每次查询都是根据请求参数组装成一条SQL,查出需要的object select object, etag, size,…… from meta where bucket_id = * and shard_key = * order by object limit 0,1001
    • 针对子文件夹的折叠处理:
      • ls操作需要支持折叠文件夹操作,以免一个文件夹下边有很多子文件夹,而且子文件夹里有很多文件的时候,会导致多次ls也一直在一个子文件夹里边,支持折叠子文件夹,用户才可以ls出文件夹下边的所有子文件夹
      • 解决方案:当遇到子文件夹中文件多的时候,每次都能list出指定数目的objects,这些objects可能都是一个子文件中的文件;因为用户需要折叠子文件夹,因此我们进行跳过子文件夹的处理,极端情况下这批objects都在一个子文件夹中,此时我们需要根据最后一个object name来判断是否在子文件中,如果该批objects在子文件中,此时我们会对下次查询的起始位置做一个++操作,确保下次查询跳过这个已经获取了的子文件夹;如此进行三次重试,至少保证一次请求可以拿出3个子文件夹给用户
    • marker的处理:如果需要跳过文件夹,对marker++;例如正常情况下,object > marker;如果要跳过文件夹,变成object >= marker++
  • 核心代码

    // 需要跳过文件夹时对marker的处理
    string BucketModel::ProcessMarkerWithDelimiter(const string& marker,
                        const string& prefix, char delimiter) {
        //prefix empty, or prefix not empty and marker start with prefix
        if (prefix.empty() || marker.compare(0, prefix.length(), prefix) == 0) {
            size_t pos = marker.find_first_of(delimiter, prefix.length());
            string new_marker;
            if (std::string::npos != pos) {
                ++pos;
                new_marker = marker.substr(0, pos);
                new_marker[new_marker.length()-1]++;
                return new_marker;
            } else {
                return marker;
            }
        }
        return marker;
    }
    

多表list

  • 解决方案

    • 利用Golang携程并发发送1024个shardMeta的list请求,也就是1024次SQL查询,拿到1024个请求结果之后,对结果做归并和提取处理,最终得出与单表listObject接口逻辑一致的结果。
  • 核心代码

    // merge two ObjectInfo struct type slice, max length is maxKeys
    func merge(maxKeys int, left, right []ObjectInfo) []ObjectInfo {
        var result []ObjectInfo
    
        for len(left) > 0 || len(right) > 0 {
            if len(left) == 0 {
                result = append(result, right...)
                break
            }
            if len(right) == 0 {
                result = append(result, left...)
                break
            }
            if left[0].Key <= right[0].Key {
                result = append(result, left[0])
                left = left[1:]
            } else {
                result = append(result, right[0])
                right = right[1:]
            }
            if len(result) >= maxKeys {
                return result[0:maxKeys]
            }
        }
        if len(result) >= maxKeys {
            return result[0:maxKeys]
        }
        return result
    }
    
  • 方案重难点

    • Cache嗅探机制:
      List ShardMeta每次会通过VIP从Bucket取回1024个表数据,为了提高请求响应速度,当请求频率达到阈值时,这1024个表的数据会分别以ApiType+Bucket+ShardKey为key保存到Cache中,如何在充分利用Cache降低请求响应速度和对数据库压力的同时,也尽量减少与Cache之间的通信显得尤为重要。

      在充分考虑了系统Cache的实现机制之后,设计了预读取方案来降低与Cache之间的通信次数,将1024个ShardMeta的请求任务队列切割出一小部分(10个)作为嗅探Cache的任务,多个goroutine并发去读Cache,如果嗅探Cache的ShardMeta都能读取到,说明Cache中能读到1024个表数据,可以继续读Cache,否则后续请求都直接请求数据库,不再读Cache,这样可以把每次List Shard请求读Cache的次数从1024次降低到10次。

    • 性能优化:
      List ShardMeta是一个重请求,相当于所有操作开销都放大1024倍,然而又必须满足客户对耗时的要求,我主要从以下几个方面做了性能优化

      减少互斥锁粒度,充分利用Golang atomic函数:在归并1024个表数据时,只给归并过程和写最终结果的少量代码加锁,利用atomic变量做全局信号量,保证变量操作的原子性

      并发Goroutine发送请求和进行Json-Struct转换:利用Golang Goroutine给Bucket并发发请求,加快网络请求速度;归并结果时需要多次进行byte数组和Struct之间的转换,而Golang自带json库函数效率低,也将该过程利用Goroutine来并发完成,加快转换速度

      通过VIP分发请求:1024个请求如果并发到本机Bucket服务,会对本机服务造成巨大压力,而且由于单进程资源有限,会导致响应速度很慢。因此采用了将1024个请求发送给VIP的方式,均衡地分散到服务集群数千台机器上,保证了处理速度,降低了单机压力

    • 异常处理机制:
      List ShardMeta每次1024个数据库请求,如果有一个请求遇到网络超时等错误,返回结果不正常,就会导致整个请求返回结果有误;因此我采用了标志量和超时机制来实现异常处理,一旦单个请求出现异常,标志量置位TRUE,其他请求不再继续;同时采用Goroutine等待超时信号量的方式来处理超时情况,如果工作任务出现错误,一旦从任务通道取不到任务超过50ms,Goroutine自动结束操作,给用户返回错误提示

json转换函数性能低的解决方案

Golang自带json Unmarshal函数转化包含1000个obj的list object接口返回结果时,耗时10ms,效率很低;主要有两种解决方案:

  • 一种是减少转化次数以及缩小锁的范围,并发地去做转化
  • 另一种是使用开源库,性能能够提升2-4倍,例如easyjson,ffjson

并发等待是等goroutine还是Channel

Goroutine一般会结合WaitGroup来使用,wg相当于一个同步信号量,等到wg减到0,才开始下一步的逻辑

  • 如果wg等待goroutine,也就是说先给wg.add(Goroutine数目),然后使用defer this.wg.Done()和return来指定当函数结束(也意味着Goroutine结束)时就给wg减一,这样能确保最终协程都结束时,wg也不再等待,开始下一步的逻辑
  • 如果wg等待channel里边的任务,会存在一些异常问题,例如channel任务没被消费完,Goroutine都异常中止了,那么wg永远等不到减到0的那一刻,程序就会hang住了

耗时正比例增加因为多打了log

上线之后从某一天开始发现list请求耗时突增到上线时的6倍,非常奇怪,而且耗时随着每次请求数据量的增加而增加,经过git diff版本分析代码,发现是后续debug增加了几行日志打印,打印数据为每个shardMeta返回的数据,而且打印日志的代码被mutex互斥锁锁住,也就意味着每次要串行打印n个object数据,打印1024次,每次耗时10ms,从而导致耗时剧增;后来删除log之后耗时恢复到正常水平

Golang相关技术和踩坑

defer和WaitGroup
  1. 在Golang中,defer表达式通常用来处理一些清理和释放资源的操作。defer后面的表达式会被放入一个列表中,在当前方法返回的时候,列表中的表达式就会被执行。一个方法中可以在一个或者多个地方使用defer表达式
  2. defer表达式中变量的值在defer表达式被定义时就已经明确
  3. defer表达式的调用顺序是按照先进后出的方式
  4. defer还可以用于在 return 之后修改函数的返回值
  5. Go语言中, panic用于抛出异常, recover用于捕获异常.

参考链接

1.Golang中defer的那些事

Linux终端利器tmux指南

发表于 2017-12-07 | 更新于 2017-12-30 | 字数统计1.4k字 | 阅读时长5分钟

tmux简介

tmux是BSD实现的Screen替代品,相对于Screen,它更加先进:支持屏幕切分,而且具备丰富的命令行参数,使其可以灵活、动态的进行各种布局和操作。它可以做到一条命令就启动起来(强大的配置)

安装方法
系统 安装命令
mac brew install tmux(前提需要安装homebrew)
ubuntu sudo apt-get install tmux
centos yum install -y tmux
其它 下载源码配置-编译-链接-安装
概念名词

在你输入tmux开启了tmux服务器后,会首先创建一个会话,而这个会话则会首先创建一个窗口,其中仅包含一个面板; 也就是说,这里看到的所谓终端控制台应该称作tmux的一个面板,虽然其使用方法与终端控制台完全相同。

概念 说明
server服务器 输入tmux命令时就开启了一个服务器
session会话 一个服务器可以包含多个会话
window窗口 通过c-b-c创建,所有窗口的名称显示在底部状态栏上,一个会话可以包含多个窗口
pane面板 窗口里面的分屏,可通过c-b-%创建,一个窗口可以包含多个面板
使用场景和优点
  1. tmux命令开启一个终端之后,可以在这个终端里开启多个windows,同时还可以把windows split成多个pane,并行工作
  2. ssh登录远程主机的情景下,一旦ssh断开,那么当前账户登录的任务就被取消了,但是使用 tmux 可以在断开之后继续工作,下次登录可以查看

常用默认快捷键

tmux里边所有快捷键都默认以c-b作为前缀,也就是以ctrl+b开头,当然你也可以自行配置;

  1. 基本命令
    • tmux创建一个tmux session
    • tmux new -s session_name创建一个叫做session_name的 session
    • tmux ls\tmux list-sessions列出现有的所有session
    • tmux info列出所有的 session, window, pane, 运行的进程号等
    • tmux list-keys列出所有可以的快捷键和其运行的 tmux 命令
    • tmux list-commands列出所有的 tmux 命令及其参数
    • tmux at(attach) -t session_name进入叫做session_name的session
    • tmux detach\c-b-d离开当前开启的session
    • tmux kill-session -t session关闭开启的session
  2. 基本操作

    • d脱离并保存当前会话,可暂时返回Shell界面,tmux仍在后台运行
    • ctrl + z挂起当前会话
    • s选择并切换会话;在同时开启了多个会话时使用
    • D选择要脱离的会话;在同时开启了多个会话时使用
    • :进入命令行模式;此时可输入支持的命令,例如 kill-server 关闭所有tmux会话
    • t显示当前的时间
  3. 窗口操作

    • c创建新窗口
    • &关闭当前窗口
    • [0-9]数字键切换到指定窗口
    • p切换至上一窗口
    • n切换至下一窗口
    • l切换到最后使用的窗口
    • l前后窗口间互相切换
    • w通过窗口列表切换窗口
    • ,重命名当前窗口,便于识别
    • .修改当前窗口编号,相当于重新排序
    • f在所有窗口中查找关键词,便于窗口多了切换
  4. (面板)分屏操作
    • ?显示快捷键帮助
    • "将当前面板上下分屏(建议设置成 |)
    • %将当前面板左右分屏(建议设置成 -)
    • x关闭当前分屏
    • q显示面板编号
    • o选择当前窗口中下一个面板
    • ;切换到最后一个使用的面板
    • 方向键移动光标选择对应面板
    • z最大化当前所在面板
    • !面板转变成窗口

推荐配置

如果你有个性化配置的需要,包括快捷键,状态栏等,那么修改~/.tmux.conf文件可以达到你的目的,让你用起来更符合自己的习惯; 也可以通过配置~/.bashrc每次ssh登录时都默认attach或者新建tmux会话

  1. tmux.conf配置快捷键,配色等
# 开启鼠标生效
set-option -g mouse on

#设置前缀为Ctrl + x
set -g prefix C-x
unbind C-b

# key bindings for horizontal and vertical panes
unbind %
bind | split-window -h      # 使用|竖屏,方便分屏
unbind '"'
bind - split-window -v      # 使用-横屏,方便分屏

# status bar with load and time
set -g status-bg blue
set -g status-fg '#bbbbbb'
set -g status-left-fg green
set -g status-left-bg blue
set -g status-right-fg green
set -g status-right-bg blue
set -g status-left-length 90
set -g status-right-length 90
set -g status-left '[#(whoami)]'
set -g status-right '[#(date +" %m-%d %H:%M ")]'
set -g status-justify "centre"
set -g window-status-format '#I #W'
set -g window-status-current-format ' #I #W '
setw -g window-status-current-bg blue
setw -g window-status-current-fg green
# pane number display
set -g display-panes-active-colour colour33 #blue
set -g display-panes-colour colour166 #orange
set -g base-index 1
set -g pane-base-index 1
set -g display-time 3000
set -g history-limit 10000

# copy-mode 将快捷键设置为 vi 模式
setw -g mode-keys vi

# 选中窗口
bind-key k select-pane -U
bind-key j select-pane -D
bind-key h select-pane -L
bind-key l select-pane -R
  1. ~/.bash_rc配置登录时自动进入tmux

    tmux_init()
    {
        tmux new-session -s "xiaoyong" -d -n "local"    # 开启一个会话
        tmux new-window -n "other"          # 开启一个窗口
        tmux split-window -h                # 开启一个竖屏
        tmux split-window -v                # 开启一个横屏
        tmux -2 attach-session -d           # tmux -2强制启用256color,连接已开启的tmux
    }
    
    # 判断是否已有开启的tmux会话,没有则开启
    if which tmux 2>&1 >/dev/null; then
        test -z "$TMUX" && (tmux attach || tmux_init)
    fi
    

参考链接

  1. tmux指南
  2. tmux如何将内容复制到系统clipboard ? Mac下如果用 iterm2 可以在 preference 下选择 Applications in terminal may access clipboard

Mysql初次Init线程不安全问题

发表于 2017-12-02 | 更新于 2017-12-03 | 字数统计1.1k字 | 阅读时长5分钟

现象描述

沙盒测试时发现的这个问题,在程序启动时偶发core,而且不稳定复现,查看core信息会发现core在了mysql连接上;而且最后发现的规律是每次程序启动时,如果多线程获取数据库连接,就可能出现core。core信息如图所示:

代码复现

在主线程内初始化mysql,在子线程内调用mysql_real_connect,就会导致coredump

#include <mysql.h>
#include <pthread.h>
void* func(void* arg)
{
    MYSQL* mysql = (MYSQL *)arg;
    mysql_real_connect(mysql, “127.0.0.1″, “root”, “123456″, “chen”, 1234, NULL, 0);
    mysql_close(mysql);
    return (void *)0;
}

int main()
{
    MYSQL mysql;
    if (NULL == mysql_init(&mysql))
    {
        return -1;
    }
    pthread_t thread;
    pthread_create(&thread, NULL, func, &mysql);
    pthread_join(thread, NULL);
    return 0;
}

出现原因

如官网文档所说,当我们调用mysql_real_connect()函数去获取数据库连接时,需要先调用mysql_init(MYSQL *mysql)函数去获取一个MYSQL connection handler,然而mysql_init()不是完全线程安全的,但是只要成功调用一次后就线程安全了,如果有多线程并发调用mysql_init(),第一次init时如果刚好多线程并发调用,就会出core;为啥第一次调用mysql_init时线程不安全?我们可以来看看mysql源码:

  1. mysql.h文件预定义mysql_library_init函数

    #define mysql_library_init mysql_server_init
    
  2. client.c文件定义mysql_init函数,并调用mysql_server_init函数

    // Init MySQL structure or allocate one
    
    MYSQL * STDCALL
    mysql_init(MYSQL *mysql)
    {
      if (mysql_server_init(0, NULL, NULL))
        return 0;
      if (!mysql)
      {
        if (!(mysql=(MYSQL*) my_malloc(sizeof(*mysql),MYF(MY_WME | MY_ZEROFILL))))
        {
          set_mysql_error(NULL, CR_OUT_OF_MEMORY, unknown_sqlstate);
          return 0;
        }
        mysql->free_me=1;
      }
      else
        memset(mysql, 0, sizeof(*(mysql)));
      mysql->charset=default_client_charset_info;
      strmov(mysql->net.sqlstate, not_error_sqlstate);
    
      /*
        Only enable LOAD DATA INFILE by default if configured with option
        ENABLED_LOCAL_INFILE
      */
    
    #if defined(ENABLED_LOCAL_INFILE) && !defined(MYSQL_SERVER)
      mysql->options.client_flag|= CLIENT_LOCAL_FILES;
    #endif
    
    #ifdef HAVE_SMEM
      mysql->options.shared_memory_base_name= (char*) def_shared_memory_base_name;
    #endif
    
      mysql->options.methods_to_use= MYSQL_OPT_GUESS_CONNECTION;
      mysql->options.report_data_truncation= TRUE;  /* default */
    
      mysql->reconnect= 0;
    
      mysql->options.secure_auth= TRUE;
    
      return mysql;
    }
    
  3. libmysql.c文件定义mysql_server_init函数, 问题就出在这个函数,用了mysql_client_init这个标记量来判断是否需要调用my_thread_init函数,如果mysql_client_init==1就直接为每个线程初始化私有变量,否则会先去初始化一些全局性的系统函数,资源和变量; 所以如果第一次init时出现多线程并发情景,线程A将mysql_client_init变量置为1,紧接着初始化全局资源,与此同时线程B走了else分支,直接开始调用my_thread_init函数,此时就会报错了,core由此产生。

    int STDCALL mysql_server_init(int argc __attribute__((unused)),
                      char **argv __attribute__((unused)),
                      char **groups __attribute__((unused)))
    {
      int result= 0;
      if (!mysql_client_init)
      {
        mysql_client_init=1;
        org_my_init_done=my_init_done;
        if (my_init())                /* Will init threads */
          return 1;
        init_client_errs();
        if (mysql_client_plugin_init())
          return 1;
        if (!mysql_port)
        {
          char *env;
          struct servent *serv_ptr __attribute__((unused));
    
          mysql_port = MYSQL_PORT;
    
          /*
            if builder specifically requested a default port, use that
            (even if it coincides with our factory default).
            only if they didn't do we check /etc/services (and, failing
            on that, fall back to the factory default of 3306).
            either default can be overridden by the environment variable
            MYSQL_TCP_PORT, which in turn can be overridden with command
            line options.
          */
    
    #if MYSQL_PORT_DEFAULT == 0
          if ((serv_ptr= getservbyname("mysql", "tcp")))
            mysql_port= (uint) ntohs((ushort) serv_ptr->s_port);
    #endif
          if ((env= getenv("MYSQL_TCP_PORT")))
            mysql_port=(uint) atoi(env);
        }
    
        if (!mysql_unix_port)
        {
          char *env;
    #ifdef __WIN__
          mysql_unix_port = (char*) MYSQL_NAMEDPIPE;
    #else
          mysql_unix_port = (char*) MYSQL_UNIX_ADDR;
    #endif
          if ((env = getenv("MYSQL_UNIX_PORT")))
        mysql_unix_port = env;
        }
        mysql_debug(NullS);
    #if defined(SIGPIPE) && !defined(__WIN__)
        (void) signal(SIGPIPE, SIG_IGN);
    #endif
    #ifdef EMBEDDED_LIBRARY
        if (argc > -1)
           result= init_embedded_server(argc, argv, groups);
    #endif
      }
      else
        result= (int)my_thread_init();         /* Init if new thread */
      return result;
    }
    
  4. 官方文档

    • mysql_real_connect() attempts to establish a connection to a MySQL database engine running on host. mysql_real_connect() must complete successfully before you can execute any other API functions that require a valid MYSQL connection handler structure.

    • MYSQL *mysql_init(MYSQL *mysql)

      Allocates or initializes a MYSQL object suitable for mysql_real_connect(). If mysql is a NULL pointer, the function allocates, initializes, and returns a new object. Otherwise, the object is initialized and the address of the object is returned. If mysql_init() allocates a new object, it is freed when mysql_close() is called to close the connection.

      In a nonmulti-threaded environment, mysql_init() invokes mysql_library_init() automatically as necessary. However, mysql_library_init() is not thread-safe in a multi-threaded environment, and thus neither is mysql_init(). Before calling mysql_init(), either call mysql_library_init() prior to spawning any threads, or use a mutex to protect the mysql_library_init() call. This should be done prior to any other client library call.

解决方案

  1. 每一次连接数据库时都先后加锁调用mysql_init()和mysql_real_connect(),确保init先于connect函数被调用过,而且不会被其他线程并发调用,以免初次连接数据库时多线程并发导致core
  2. 在程序启动时先全局调用一次mysql_init()函数,确保初次调用mysql_init时是线程安全的,之后的调用mysql_real_connect函数就不会出core
  3. MySQL提供了线程安全的库libmysql_r,可以考虑替换原来的线程不安全的libmysql库

C++基础

发表于 2017-11-11 | 更新于 2019-01-03 | 字数统计5.4k字 | 阅读时长19分钟

基础语法

  1. 在C和C++语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化而拥有不可预测的值。 C++保证了所有对象与对象成员都会初始化,但其中基本数据类型的初始化还得依赖于构造函数。
  2. 在C语言中int a;表示声明了整型a但未初始化,而C++中的对象总是会被初始化的,无论是否写了圆括号或者是否写了参数列表;定义基本数据类型变量(单个值、数组)的同时可以指定初始值,如果未指定C++会去执行默认初始化(default-initialization)。变量初始化
  3. size_t,ssize_t,int和long的区别
  4. gcc test.c -o test编译指令分为四个阶段进行,即预处理(也称预编译,Preprocessing, 把头文件内容真正引入cpp文件中)、编译(Compilation生成汇编代码.s文件)、汇编 (Assembly编译为目标文件.o)和连接(Linking包括静态连接库中需要的库文件)。汇编语言入门教程
  5. 函数库分为静态库和动态库两种。静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。无论静态库,还是动态库,都是由.o文件创建的。
    • .o 一个.c或.cpp文件对应一个.o文件, 也就是编译器编译cpp源代码文件生成的目标文件
    • .a是静态库,是好多个.o合在一起,多个.a可以链接生成一个可执行文件;静态库文件名的命名规范是以lib为前缀,紧接着跟静态库名,扩展名为.a
    • .so是动态库,文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀lib,但其文件扩展名为.so
    • 默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。

智能指针boost::scoped_ptr/std::auto_ptr

boost::scoped_ptr和std::auto_ptr(已被C++11废弃)非常类似,是一个简单的智能指针,它能够保证在离开作用域后对象被自动释放,避免由于忘记手动调用delete而造成内存泄漏。实现原理都是利用了一个栈上的对象去管理一个堆上的对象,从而使得堆上的对象随着栈上的对象销毁时自动删除。

两者区别

  1. 不能转换所有权:boost::scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的”{}”之间),无法传到区间之外,无法拷贝。这就意味着boost::scoped_ptr对象是不能作为函数的返回值的(std::auto_ptr可以)。
  2. 不能共享所有权:这点和std::auto_ptr类似。这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱——不能用于stl的容器中。
  3. 不能用于管理数组对象:由于boost::scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。
  4. 如何在他们之间取舍取决于是否需要转移所管理的对象的所有权(如是否需要作为函数的返回值)。如果没有这个需要的话,大可以使用boost::scoped_ptr,让编译器来进行更严格的检查,来发现一些不正确的赋值操作。

C++ 显式类型转换

分别为转换方式,目标类型,被转换的值。
1
2
3
4
5
6
7

1. ```static_cast<type>(expression)```: 不提供运行时的检查的类型转换方式
* 主要用于父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类指针/引用,这种转换是不安全的
* 用于基本数据类型之间的转换,例如把int转char,int转enum等,需要编写程序时来确认安全性
* 把void指针转换成目标类型的指针(这是极其不安全的)

2. ```dynamic_cast<type>(expression)```: 在运行时检查类型转换是否合法,具有一定的安全性,额外消耗一些性能;仅适用于指针或引用转换,若指针转换失败,则返回空指针,若引用转换失败,则抛出异常。经常用于将指针转换为void*: ```A *pA = new A; void *pV = dynamic_cast<void *>(pA);
  1. 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
    4. ```reinterpret_cast```非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。

    ### 守护进程
    #### Supervisor
    1. Supervisor是一个C/S系统, 允许用户监控和管理一系列运行于类Unix系统上的进程。通常,我们使用Supervisor将一个普通的命令行进程变为后台daemon,并监控这一些进程,以便这些进程在意外退出后能够重新启动。
    2. Supervisor有以下优点:
    * 简单, ini风格的配置文件,容易阅读,对每个进程都提供了许多选项以便失败重启和日志轮替。
    * 集中,一站式的进程启动,停止,监控平台。进程可以单独管理或分组管理,更提供了Web管理界面。
    * 高效,通过fork/exec启动子进程且子进程不会进入守护模式,当子进程意外终止时,操作系统会立即发送信号给Supervisor,而不像其他方案那样,需要通过PID轮询。
    * 可扩展,拥有非常简单的事件通知协议,任何编程语言程序都能监控它,也提供了XML-RPC管理接口,也为Python开发者预留了扩展空间。
    兼容性,除了Windows,几乎可以运行于任何操作系统,已经测试过的包括Linux, Mac OS X, Solaris, 和 FreeBSD。
    * 久经考验,虽然开发非常活跃,但Supervisor并不是新软件,它已经存在多年,广泛运行于许多服务器上。

    #### systemd
    1. Systemd是一个Linux操作系统下的系统和服务管理器。它被设计成向后兼容SysV启动脚本,并提供了大量的特性,如开机时平行启动系统服务,按需启动守护进程,支持系统状态快照,或者基于依赖的服务控制逻辑,已成为大多数发行版的标准配置。可以通过 systemctl --version 命令来查看使用的版本
    3. systemd 和 supervisord 各有长短,不存在哪一方绝对的碾压。systemd 跟 Linux 紧密结合,所需的依赖少,其提供的保障自然比 supervisord 更可靠。然而在强大的能力背后,也有配置复杂、不易上手等问题。

    ### 常用测试工具
    1. Apache自带ApacheBench程序对Apache或其它类型的服务器进行网站访问压力测试。

    > ApacheBench命令原理:ab命令会创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的,因此,既可以用来测试Apache的负载压力,也可以测试nginx、lighthttp、tomcat、IIS等其它Web服务器的压力。

    > ab命令对发出负载的计算机要求很低,既不会占用很高CPU,也不会占用很多内存,但却会给目标服务器造成巨大的负载,其原理类似CC攻击。自己测试使用也须注意,否则一次上太多的负载,可能造成目标服务器因资源耗完,严重时甚至导致死机。

    2. [服务器性能评估相关指标QPS](https://ruby-china.org/topics/26221)

    ### C++常用错误调试工具

    #### core dump
    core dump是大多数UNIX系统实现的一种特性,当进程运行的过程中异常终止或崩溃时,操作系统会将进程当前的内存映像和一部分相关的调试信息写入core文件,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。我们可以使用```ulimit -c unlimited```命令来开启core dump。

    Linux中信号是一种异步事件处理的机制,每种信号对应有其默认的操作,默认操作主要包括忽略该信号(Ingore)、暂停进程(Stop)、终止进程(Terminate)、终止并发生core dump(core)等;```kill -l```可以看到linux里边的信号种类,默认动作中可能产生core文件的信号包括以下种类:

    信号名字 | 说明 | 默认动作
    --- | --- | ---
    SIGABRT(6) | 异常终止(调用abort函数产生此信号)| 终止+core
    SIGBUS(7) | 硬件故障,比如出现某些内存故障 | 终止+core
    SIGEMT | 硬件故障 | 终止+core
    SIGFPE(8) | 算术异常,比如除以0,浮点溢出等 | 终止+core
    SIGILL(4) | 非法硬件指令 | 终止+core
    SIGIOT(29) | 硬件故障 | 终止+core
    SIGQUIT(3) | 终端退出符,比如Ctrl+C | 终止+core
    SIGSEGV(11) | 无效内存引用 | 终止+core
    SIGSYS(31) | 无效系统调用 | 终止+core
    SIGXCPU(24) | 超过CPU限制(setrlimit)| 终止+core/忽略
    SIGXFSZ(25) | 超过文件长度限制(setrlimit)| 终止+core/忽略

    这就是为什么我们使用```ctrl+z```来挂起一个进程或者```Ctrl+C```结束一个进程均不会产生 core dump,因为前者会向进程发出SIGTSTP信号,该信号的默认操作为暂停进程(Stop Process);后者会向进程发出SIGINT信号,该信号默认操作为终止进程(Terminate Process)。同样上面提到的 kill -9 命令会发出 SIGKILL 命令,该命令默认为终止进程。而如果我们使用 Ctrl+\ 来终止一个进程,会向进程发出 SIGQUIT 信号,默认是会产生 core dump 的。还有其它情景会产生core dump, 如:程序调用 abort() 函数、访存错误、非法指令等等。

    #### dmesg
    **段错误**是指访问的内存超出了系统给这个程序所设定的内存空间, 例如访问了不存在的内存地址,访问了系统保护的内存地址,访问了只读的内存地址等等情况。```dmesg```命令显示linux内核的环形缓冲区信息,我们可以从中获得诸如系统架构、cpu、挂载的硬件,RAM等多个运行级别的大量的系统信息。通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。

    1. dmesg信息解读: 如下是bucket bin文件段错误信息,ip信息会用到后续objdump反编译分析

    ```bucket[14306]: segfault at b8c6000 ip 0000000000c3dece sp 00007ffd2c96ae80 error 6 in bucket[400000+eea000]

error number是6, 转成二进制就是110, 即bit2=1, bit1=1, bit0=0, 按照上面的解释,我们可以得出这条信息是由于用户态程序写操作访问越界,访问的是无效地址造成的。

error number是由三个字位组成的,从高到底分别为bit2 bit1和bit0,所以它的取值范围是0~7.

  • bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
  • bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
  • bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址
  1. addr2line -e 进程名 c3dece; 使用addr2line命令获取出错具体行号

objdump

objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,使用objdump生成二进制的相关信息,并重定向到文件中

-d(-S尽可能完全反编译) bin/bucket > segfaultDump```
1
2

```grep -n -A 10 -B 10 "c3dece" ./segfaultDump

pstack和strace

strace跟踪程序使用的底层系统调用,可输出系统调用被执行的时间点以及各个调用耗时;pstack工具对指定PID的进程输出函数调用栈。一般两者配合调用,先用strace查看耗时的系统调用,然后用pstack看函数调用栈信息,找到出问题的函数。

  1. 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    	
    # -f -F选项设置同时跟踪fork和vfork出来的进程,输出到~/straceout.txt,myserver是要启动和调试的程序
    strace -f -F -o ~/straceout.txt myserver

    # 跟踪28979进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示)
    strace -o output.txt -T -tt -e trace=all -p 28979
    2. ```pstack pid```: print a stack trace of a running process
    3. [实例讲解如何使用strace+pstack利器分析程序性能](https://blog.csdn.net/jiang1013nan/article/details/17558165)

    #### 其它
    1. ldd:使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。例如```ldd bin/bucket

  2. nm:使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。

    bin/bucket```
    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


    #### 参考链接
    1. [Linux工具](http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/objdump.html)



    ### condition_variable/mutex/thread_pool

    1. **条件变量(Condtion Variable)是在多线程程序中用来实现“等待->唤醒”逻辑常用的方法,条件变量能使一个或多个线程进入阻塞状态,直到接到另一个线程的通知(notify),或者发生超时或虚假唤醒时,才退出阻塞**。例如,应用程序A中包含两个线程t1和t2。t1需要在bool变量test_cond为true时才能继续执行,而test_cond的值是由t2来改变的,这种情况下可供选择的方案有两种:

    * t1定时的去轮询变量test_cond,如果test_cond为false,则继续休眠;如果test_cond为true,则开始执行。
    * 利用条件变量,t1在test_cond为false时调用cond_wait进行等待,t2在改变test_cond的值后,调用cond_signal,唤醒在等待中的t1,告诉t1 test_cond的值变了,这样t1便可继续往下执行。
    * Wait_for: 在条件变量收到信号或者指定的超时发生前,线程一直处于阻塞状态;
    * Wait_until:在条件变量收到信号或者指定的时刻到达之前,线程一直处于阻塞状态。

    2. **Thread相关的函数**:

    * t.join():使调用线程(本例是指主线程)一直处于阻塞状态,直到正在执行的线程t执行结束。如果线程函数返回某个值,该值也将被忽略。不过,该函数可以接收任意数量的参数,尽管可以向线程函数传递任意数量的参数,但是所有的参数应当按值传递。如果需要将参数按引用传递,那要向下例所示那样,必须将参数用std::ref 或者std::cref进行封装。

    ```cpp
    void func(int i, double d, const std::string& s)
    {
    std::cout << i << ", " << d << ", " << s << std::endl;
    }

    void func1(int& a)
    {
    a++;
    }

    int main()
    {
    std::thread t(func, 1, 12.50, "sample");
    t.join();

    int a = 42;
    std::thread t1(func, std::ref(a));
    t1.join();

    return 0;
    }

    • Detach: 允许执行该方法的线程脱离其线程对象而继续独立执行。脱离后的线程不再是可结合线程(你不能等待它们执行结束)。

    • get_id: 返回当前线程的id

    • yield:在处于等待状态时,可以让调度器先运行其他可用的线程。

    • sleep_for:阻塞当前线程,时间不少于其参数指定的时间。

    • sleep_util:在参数指定的时间到达之前,使当前线程一直处于阻塞状态。

  3. std::recursive_mutex:允许同一个线程多次获取同一个互斥量,可获取的互斥量的最大次数并没有具体说明。但是一旦超过最大次数,再对lock进行调用就会抛出std::system_error错误异常。

  4. Timed_mutex和Recursive_timed_mutex:try_lock_for() 和 try_lock_until(),分别用于在某个时间段里或者某个时刻到达之间获取该互斥量

  5. Lock_guard: 在构造对象时,它试图去获取互斥量的所有权(通过调用lock()),在析构对象时,自动释放互斥量(通过调用unlock()).这是一个不可复制的类。

  6. Unique_lock: 这个一通用的互斥量封装类,不同于lock_guard,它还支持延迟加锁,时间加锁和递归加锁以及锁所有权的转移和条件变量的使用。这也是一个不可复制的类,但它是可移动类。

  7. C++11 中的线程、锁和条件变量

Linux中的线程同步机制-Futex/Mutexes/Condition Variables/POSIX Semaphores

Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享 的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不 用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。
Linux多线程同步机制参考链接

C++的RAII —- 资源获取即初始化

C++的一种利用C++构造的对象最终会被销毁的原则来管理资源避免泄露的用法。具体做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。

如何使用RAII ?
把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。例如对socket、互斥锁、文件句柄和内存的申请和释放。

其它

  1. C++ 11 auto关键字编译时类型推断。
  2. std::map::emplace_hint 暗示指定位置前插入元素;make_shared构造类型对象和make_pair构造pair
  3. 常用第三方库:boost,gflag,glog,folly,chromium,bazel代码构建
  4. 性能分析和内存泄漏:gperftools
  5. C++单例:std::atomic + DCLP, std::once_flag
  6. 线程池:rocksdb/util/thread_pool_imp.h
  7. C++智能指针:boost::smart_ptr, std::shared_ptr, std::unique_ptr
  8. boost::shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。共享指针对象重载了 operator* 和 operator-> , 所以你可以像通常的指针一样使用它. shared_ptr
  9. C++左值与右值引用: C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。左值的声明符号为”&”, 为了和左值区分,右值的声明符号为”&&”。右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。
  10. C++11之后支持lamda语法:std::function可调用实体,std::bind返回可调用实体,用于后续作为回调函数
1…456…18
CharlesXiao

CharlesXiao

在码农炼成之路不断挣扎……stay hungry……keep learning……

87 日志
18 分类
78 标签
github weibo Daijiale的个人站点
推荐阅读
  • RocksDB
  • Google FE
© 2015.05.16 – 2019 CharlesXiao
本站总访问量:
|
总访客数:
|
博客全站共169.6k字