Linux之统计ip排行

面试或者笔试中,经常会出现一道题,那就是,统计nginx日志中,访问量前N个的IP地址。

自问我对Linux还算是比较了解,知道过几个命令,但是组装起来用,确实是一门学问。

以下是我盲写的执行结果

执行结果,好像并不进入人意,不可能访问量最高的ip是9次。

上面的命令拆分起来,可以理解为:

打印第一列->去重,并且统计->倒叙排列->取前10条

看起来,好像没什么问题,但是问题出在了uniq上面了

uniq其实只用了去掉相邻的重复记录的,也就是,我们在使用uniq的时候,其实是应该先按照排序,然后再进行去重操作

改正后的命令如下

相比起前一个命令,第2个sort还增加了 -n,这是因为,我们使用uniq的时候,进行了计数,我们再根据计数(-n)进行倒叙排列(-r),最后前10条(head -10)记录.

携程Apollo配置中心初探

1. 背景

平时开发最头疼之一就是各种配置:

  1. 一个项目往往会包含各式各样的配置信息,且不说数据库、redis、memcache这些常用的配置,还会有很多业务上的配置。
  2. 线上、测试和开发环境配置各不一样,每个环境都要保存一份
  3. 每次上线的时候,都要挨个check一下,
  4. 更改某个配置,需要重新上线代码
  5. ….

所以配置中心,在devops的开发中,是必不可少的,配置中心,也可以有效的避免,因为更改配置代码,导致的代码运行出错的风险。

2. Apollo

携程的Apollo配置中心,在业内算是比较有名的,github上面大概有8.5k的star。很多知名的公司也都在使用。至于实现的原理直接看github上面的wiki即可。

3. DO IT

3.1 创建项目

3.1.1 创建项目,clone项目

3.1.2 按需更改docker-compose.yml

feilongdeMacBook-Pro:apollo2 feilong$ cd scripts/docker-quick-start/
feilongdeMacBook-Pro:docker-quick-start feilong$ ll
total 8
-rw-r--r--  1 feilong  wheel  663 Oct 27 14:56 docker-compose.yml
drwxr-xr-x  4 feilong  wheel  128 Oct 27 14:56 sql

因为我不是java技术栈,所以以docker运行。

本地由于8080端口被占用,所以把端口改为8082

# docker-compose.yml
version: '2'

services:
  apollo-quick-start:
    image: nobodyiam/apollo-quick-start
    container_name: apollo-quick-start
    depends_on:
      - apollo-db
    ports:
      - "8082:8080"
      - "8070:8070"
    links:
      - apollo-db

  apollo-db:
    image: mysql:5.7
    container_name: apollo-db
    environment:
      TZ: Asia/Shanghai
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    depends_on:
      - apollo-dbdata
    ports:
      - "13306:3306"
    volumes:
      - ./sql:/docker-entrypoint-initdb.d
    volumes_from:
      - apollo-dbdata

  apollo-dbdata:
    image: alpine:latest
    container_name: apollo-dbdata
    volumes:
      - /var/lib/mysql

通过docker-compose启动Apollo服务

feilongdeMacBook-Pro:docker-quick-start feilong$ docker-compose up -d
Creating network "docker-quick-start_default" with the default driver
Creating apollo-dbdata ... done
Creating apollo-db     ... done
Creating apollo-quick-start ... done
feilongdeMacBook-Pro:docker-quick-start feilong$

访问地址 localhost:8070,如果启动失败的话,请参考 #1473 docker_quick_start 起不来

3.2 配置

Apollo内置了账号 apollo/admin,登录之后,可以看到有个默认的应用SampleApp

通过后台创建一个用于演示的redis的配置信息

创建结束后,点击“发布”按钮,发布最新的配置

3.3 运行测试

除了配置的server端,还要有接受配置的client端,我是PHP技术栈,所以就以PHP为主

3.3.1 初始化项目

配置php项目,通过composer引入Apollo的SDK

feilongdeMacBook-Pro:apollo feilong$ composer require multilinguals/apollo-client --ignore-platform-reqs
Using version ^0.1.1 for multilinguals/apollo-client
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
 - Installing multilinguals/apollo-client (v0.1.1): Downloading (100%)
Writing lock file
Generating autoload files

新建一个pull.php,为了能够实时获取最新的配置,需要长时间运行。

<?php
require_once 'vendor/autoload.php';

use Org\Multilinguals\Apollo\Client\ApolloClient;

// docker-compose里面配置的API服务的端口
$serverIp = '192.168.1.72:8082';
// 在Apollo的后台可以查到
$appId = 'SampleApp';
$namespaces = array('application');
$apollo = new ApolloClient($serverIp, $appId, $namespaces);
$apollo->save_dir = 'config';

$restart = true; //auto start if failed
do {
    $error = $apollo->start();
    if ($error) {
        echo('error:'.$error."\n");
    }
} while ($error && $restart);

3.3.2 运行项目

新建一个config的目录,运行pull.php文件,获取配置信息

新建一个窗口,查看下config文件夹下的文件

发现新建了一个配置文件,里面是相应的配置信息

后台做一些更新操作

发布配置,查看配置文件

4. 总结

通过配置中心,我们就不用在部署的时候,手动一直更改项目的配置文件,可以实现自动化,降低人为风险。

此外,Apollo是版本控制的,支持回滚操作,这样,就算是出现手误,也能及时回滚配置,及时生效。

docker之命名空间

1. 基本架构

docker目前采用了标准的C/S架构。客户端和服务端既可以运行在一个机器上,又可以通过socket或者restful API来进行通信。

1.1 服务端

docker服务端一般都是在宿主机上,来接受客户端的命令。docker默认使用套接字的方式,但是也是允许使用tcp进行端口的监听,可以使用docker daemon -H IP:PORT的方式进行监听。

1.2 客户端

docker的客户端主要作用是向服务端发送操作的指令。客户端默认也是采用套接字的方式,向本地的docker服务端发送命令。当然,客户端也是可以使用tcp的方式进行发送指令,使用docker -H tcp://IP:PORT,用来指定接收命令的docker服务端。

2. 命名空间

大家在平时使用Linux或者macos的时候,我们并没有拆分多个环境的需求。但是在服务器上面,加入一台服务器运行多个进程,进程之间是相互影响的,比如共享内存,操作相同的文件。我们其实更希望能够将这些进程分离开,这样情况下,如果服务受到攻击,不会影响其他的服务。

docker目前主要有6命名空间的隔离方式

2.1 进程空间隔离

进程在操作系统中是一个很重要的概念,也就是大家认为的正在运行中的程序。

feilongdeMBP:~ feilong$ ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 9:31下午 ?? 0:10.07 /sbin/launchd
0 44 1 0 9:31下午 ?? 0:00.65 /usr/sbin/syslogd
0 45 1 0 9:31下午 ?? 0:01.37 /usr/libexec/UserEventAgent (System)
0 48 1 0 9:31下午 ?? 0:00.25 /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
0 49 1 0 9:31下午 ?? 0:02.57 /usr/libexec/kextd
0 50 1 0 9:31下午 ?? 0:02.40 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd
0 52 1 0 9:31下午 ?? 0:00.16 /System/Library/PrivateFrameworks/MediaRemote.framework/Support/mediaremoted
55 55 1 0 9:31下午 ?? 0:00.38 /System/Library/CoreServices/appleeventsd --server
0 56 1 0 9:31下午 ?? 0:00.75 /usr/sbin/systemstats --daemon

可见当前系统运行了很多“程序”。

我们现在新建一个容器,然后进入容器看下,docker容器里面的进程列表

feilongdeMBP:~ feilong$ docker run -it --rm --name test busybox
/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    6 root      0:00 ps -ef

对比很明显,容器内部只有很少的几个正在运行的进程。

我们新建一个窗口,然后看下宿主机上面和docker相关的进程

localhost:~ feilong$ ps -ef | grep docker
    0    82     1   0  9:31下午 ??         0:00.02 /Library/PrivilegedHelperTools/com.docker.vmnetd
  501   918   879   0 10:26下午 ??         0:00.14 /Applications/Docker.app/Contents/MacOS/com.docker.supervisor -watchdog fd:0
  501   920   918   0 10:26下午 ??         0:03.32 com.docker.osxfs serve --address fd:3 --connect vms/0/connect --control fd:4 --log-destination asl
  501   921   918   0 10:26下午 ??         0:00.73 com.docker.vpnkit --ethernet fd:3 --port fd:4 --diagnostics fd:5 --pcap fd:6 --vsock-path vms/0/connect --host-names host.docker.internal,docker.for.mac.host.internal,docker.for.mac.localhost --gateway-names gateway.docker.internal,docker.for.mac.gateway.internal,docker.for.mac.http.internal --vm-names docker-for-desktop --listen-backlog 32 --mtu 1500 --allowed-bind-addresses 0.0.0.0 --http /Users/feilong/Library/Group Containers/group.com.docker/http_proxy.json --dhcp /Users/feilong/Library/Group Containers/group.com.docker/dhcp.json --port-max-idle-time 300 --max-connections 2000 --gateway-ip 192.168.65.1 --host-ip 192.168.65.2 --lowest-ip 192.168.65.3 --highest-ip 192.168.65.254 --log-destination asl --udpv4-forwards 123:127.0.0.1:59434 --gc-compact-interval 1800
  501   922   918   0 10:26下午 ??         0:01.17 com.docker.driver.amd64-linux -addr fd:3 -debug
  501   928   922   0 10:26下午 ??         2:40.08 com.docker.hyperkit -A -u -F vms/0/hyperkit.pid -c 2 -m 2048M -s 0:0,hostbridge -s 31,lpc -s 1:0,virtio-vpnkit,path=vpnkit.eth.sock,uuid=246fb3f9-3ad5-4683-837a-33ac39f57f25 -U 5a3669ae-b209-443a-a074-312cd32a258a -s 2:0,ahci-hd,/Users/feilong/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw -s 3,virtio-sock,guest_cid=3,path=vms/0,guest_forwards=2376;1525 -s 4,ahci-cd,/Applications/Docker.app/Contents/Resources/linuxkit/docker-for-mac.iso -s 5,ahci-cd,vms/0/config.iso -s 6,virtio-rnd -s 7,virtio-9p,path=vpnkit.port.sock,tag=port -l com1,autopty=vms/0/tty,asl -f bootrom,/Applications/Docker.app/Contents/Resources/uefi/UEFI.fd,,
  501  2074  1102   0 11:21下午 ??         0:00.50 /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper /Users/feilong/.vscode/extensions/peterjausovec.vscode-docker-0.1.0/node_modules/vscode-languageclient/lib/utils/electronForkStart /Users/feilong/.vscode/extensions/peterjausovec.vscode-docker-0.1.0/node_modules/dockerfile-language-server-nodejs/lib/server.js --node-ipc --node-ipc --clientProcessId=1102
  501  2100  1065   0 11:24下午 ttys001    0:00.12 docker run -it --rm --name test busybox
  501  2086  2083   0 11:21下午 ttys002    0:00.19 docker exec -it 910aa64a312b3a884f4efb059e47ee601bbd3ba3d62f4c92abd4120cff770828 /bin/sh
  501  2090  2087   0 11:21下午 ttys003    0:00.12 docker exec -it 73f8fbcc50651fd4fea9fe0be7fe4066ea78efd7e9b2438fe657a3e7725e7903 /bin/sh
  501  2115  2111   0 11:27下午 ttys004    0:00.00 grep docker

在进程列表中,我们没有看到容器内部运行的进程,说明相对于容器的“外部”,容器“内部”的进程是隔离的。但是我们也可以发现,刚刚创建的名字为test的容器,实质上就是宿主机上面的一个PID为2090的进程。

所以,我们可以理解docker的进程树是这个状态:

2.2 网络空间隔离

容器其实不能完全和宿主机器隔离网络,要不然的话容器就没办法通过外部进行访问,那么也就没有实际的意义。但是容器之间是网络隔离的,这种隔离的方式,就是通过网络命名空间实现的。

docker有四种不同的网络模式:Host、Container、None和bridge

docker默认的是桥接模式。

docker在创建容器的时候, 不仅会给容器创建IP地址,还会在宿主机上面创建一个虚拟网桥docker0,在运行的时候,将容器和该网桥进行相连。

在默认的情况下,创建容器的时候,都会创建一对虚拟网卡,两个虚拟网卡组成数据通道,一个在容器内部,另外一个加入到docker0的网桥中。

打开两个窗口,分别创建redis和redis2容器

[root@izj6c9b96ia369l2i47yq3z feilong]# docker run -it --rm --name redis  -p 6379:6379 redis:latest /bin/bash
root@d89535b59b0b:/data#
[root@izj6c9b96ia369l2i47yq3z feilong]# docker run -it --rm --name redis2 -p 6378:6379 redis:latest /bin/bash
root@7736850135af:/data#

打开第三个窗口,查看网桥的状态

[feilong@izj6c9b96ia369l2i47yq3z ~]$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.024219a15f9d       no              veth8331b03
                                                        vethc5f3cb9

docker0 会为每一个容器分配一个新的 IP 地址并将 docker0 的 IP 地址设置为默认的网关。网桥 docker0 通过 iptables 中的配置与宿主机器上的网卡相连,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。同时也会在防火墙加上一条新的规则。

[root@izj6c9b96ia369l2i47yq3z feilong]# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.17.0.2           tcp dpt:6379
ACCEPT     tcp  --  anywhere             172.17.0.3           tcp dpt:6379
ACCEPT     tcp  --  anywhere             172.17.0.4           tcp dpt:http
......

2.3 挂载点命名空间

docker已经可以通过命名空间将网络和进程进行隔离。挂载命名空间,允许不同的容器,查看到不同的文件结构,这样,每个命名空间的进程所看到的文件目录彼此被隔离。每个容器内的进程只会更改容器内部的文件目录。

2.4 IPC命名空间

容器中的进程交互采用的是Linux中常见的进程间交互方式(Interprocess Communication, IPC),包括信号量、消息队列和内存共享等。IPC命名空间和PID命名空间可以组合使用,同一个IPC命名空间的进程可以彼此可见,允许进行交互,不同空间的进程无法交互。

2.5 UTS 命名空间

UTS(Unix time-sharing system)命名空间允许每个容器拥有一个独立的主机名和域名,从而可以虚拟出一个独立的主机名和网络空间的环境,就可以跟网络上的一台独立主机一样。

默认情况下,docker的主机名是容器的id

2.6 用户命名空间

每个容器内部都有不同的用户组和组id,也就是说可以在容器内部使用特定的内部用户执行程序,而不是宿主机上的用户。每个容器都有root账号,但是和宿主机都不在一个命名空间。通过使用命名空间隔离,来保证容器内部用户无法操作容器外部的操作权限。

3. 总结

6种命名空间让容器之间松耦合,也让容器与宿主机松偶尔。同时,也保证了安全性。容器内部不能操作其他容器内部的东西,docker的这种命名空间隔离的方式,也比较符合Linux的系统设计。

docker之运行golang

众所周知,docker解决了编程的痛点问题——运行环境,所以我先走基本上尽量都使用docker运行。这样做,首先就是让我不必关心配置复杂的运行环境,另外也可以让我更加熟练的使用docker。

示例程序

//go-sample.go
package main
import "fmt"
func main() {
	fmt.Println("hello world");
}

Golang:onbuild

现在关于go的docker镜像也发布了很多个版本,我们首先介绍一下golang:onbuild以及如何使用。
golang:onbuild是go语言官方发布的一款很小的镜像(只有几KB大小),目的是为了让我们可以编译go文件,并且运行。使用的方式很简单,只需要创建一个Dockerfile,然后在首行加上FROM golang:onbuild

-rw-r--r--@ 1 feilong wheel 20 9 2 00:23 Dockerfile
-rw-r--r-- 1 feilong wheel 72 9 2 00:03 go-sample.go
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ cat Dockerfile
FROM golang:onbuild
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker build -t golang_onbuild .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM golang:onbuild
onbuild: Pulling from library/golang
ad74af05f5a2: Pull complete
2b032b8bbe8b: Pull complete
a9a5b35f6ead: Pull complete
25d9840c55bc: Pull complete
d792ec7d64a3: Pull complete
be556a93c22e: Pull complete
3a5fce283a1e: Pull complete
0621865a0c2e: Pull complete
Digest: sha256:c0ec19d49014d604e4f62266afd490016b11ceec103f0b7ef44875801ef93f36
Status: Downloaded newer image for golang:onbuild
# Executing 3 build triggers
 ---> Running in 109c7a7ebeb5
+ exec go get -v -d
Removing intermediate container 109c7a7ebeb5
 ---> Running in c0dfd28de95e
+ exec go install -v
app
Removing intermediate container c0dfd28de95e
 ---> 820e315d7160
Successfully built 820e315d7160
Successfully tagged golang_onbuild:latest
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker run -it --rm --name go_onbuild golang_onbuild
+ exec app
hello world

我们根据docker:onbuild的Dockerfile文件具体分析一个整个编译的过程(以1.3.1版本为例)

FROM golang:1.3.1

RUN mkdir -p /go/src/app
WORKDIR /go/src/app

# this will ideally be built by the ONBUILD below 😉
CMD ["go-wrapper", "run"]

ONBUILD COPY . /go/src/app
ONBUILD RUN go-wrapper download
ONBUILD RUN go-wrapper install

从Dockerfile和build过程可以看出,在进行build的时候,经历了三次触发器:

  1. 首先,将当前目录拷贝到. /go/src/app
  2. 下载对应的依赖包
  3. 编译安装

编译之后,golang:onbuild镜像默认包含了一个CMD [“app”] 命令,用来执行编译后的go文件。

我们通过实际run一个容器验证一下:

feilongdeMBP:go feilong$ docker run -it --rm --name golang_onbuild golang_onbuild
+ exec app
hello world

 Golang:latest

相比较golang:onbuild的便利性,golang:latest就变得很灵活了,需要我们手动编译go文件,然后手动执行编译后的文件。因为毕竟电脑并不知道你具体想要编译的顺序,以及你要想要执行的编译文件。运行过程如下:

feilongdeMBP:go feilong$ ll
total 16
-rw-r--r--@ 1 feilong  wheel  133  9  2 00:04 Dockerfile
-rw-r--r--  1 feilong  wheel   72  9  2 00:03 go-sample.go
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ cat Dockerfile
FROM golang:latest

RUN mkdir -p /go/src/app
WORKDIR /go/src/app

COPY . /go/src/app
RUN go build -o app .
CMD [ "/go/src/app/app" ]
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker build -t go_go .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM golang:latest
 ---> 7e9ac7032e33
Step 2/6 : RUN mkdir -p /go/src/app
 ---> Running in b5d3f63578ed
Removing intermediate container b5d3f63578ed
 ---> 95c2beb49121
Step 3/6 : WORKDIR /go/src/app
 ---> Running in 3011d74944c9
Removing intermediate container 3011d74944c9
 ---> 82d6a45aa3e3
Step 4/6 : COPY . /go/src/app
 ---> 475b2bdd5769
Step 5/6 : RUN go build -o app .
 ---> Running in 5802ac0c98b4
Removing intermediate container 5802ac0c98b4
 ---> 7a019370f09d
Step 6/6 : CMD [ "/go/src/app/app" ]
 ---> Running in a3f6ad19d2ef
Removing intermediate container a3f6ad19d2ef
 ---> 635417bdcda8
Successfully built 635417bdcda8
Successfully tagged go_go:latest

run一个容器,查看运行效果


feilongdeMBP:go feilong$ docker run -it --rm --name go go_go
hello world

总结

golang:onbuild和golang:lastest各有利弊,前者更加简单,能够更加简明扼要的告诉我们运行过程,而后者更加灵活,将更多的操作命令交给了开发人员。

参考文献

  • https://time-track.cn/build-minimal-go-image.html

docker之link的使用

Docker功能可以说是非常强大,但是如果想要短时间掌握docker的使用,还是有一些难度的。之前有了解一些docker的知识,但是大多数都是囫囵吞枣。并没有从最基础的知识学起,所以现在想要系统的学习一下docker的知识。

通信的痛点

link的主要作用是实现不同容器之间的连接。
举个例子,我现在有个PHP的容器,我又创建了一个mysql的容器,这个时候,如果我想要使用PHP连接mysql的容器,最常规的方式就是通过ip连接。但是这样的话,如果mysql的容器一旦重启或者重新编译,那么ip就会有可能变动,我们就需要手动更改PHP容器内连接的ip,这样的维护成本太高了。
link的作用就是要解决这个痛点问题。

link的实现

我们首先pull一个busybox的镜像,busybox是一个非常小巧的Linux镜像,占用的空间只有几MB,但是相比较Ubuntu的镜像,要小很多倍,而且也集成了很全的Linux命令

创建test1容器

从截图中,我们可以看出test1的ip是172.17.0.2

创建test2容器

我们需要新打开一个窗口,然后创建test2容器

这个时候我们ping test1的容器,是不能ping通的。

使用link关联test1和test2

我们推出test2,删除test2容器,重新run一个容器

这个时候我们发现test2里面是可以通过别名test1去进行连接,

所以,比如test1里面运行了mysql,test2里面运行了PHP,那么,连接mysql的地方,完全可以把主机的地址写成test1

总结

使用link的作用显而易见,我们可以通过别名,直接让两个容器进行通信,使用容器名称通信的优势:

  1. 不用担心ip的变动,因为name是唯一的
  2. 极大的增加了可读性
  3. 降低了运维成本

索引对性能到底有多少的影响??

索引到底对性能有多少影响?

这个问题估计是很多MySQL小白好奇的问题。当然我也是一样。因为之前的时候,并没有对索引有太多的注意,而且之前的工作经历,因为数据量很小,索引所起到的作用并不是很大,所以也没有太大注意。

事情的起点

我在公司是做后端开发(PHPer),除了日常的开发工作,也要兼职公司的运维。每周安排一个人跟进报警邮件,出现问题及时通报。

像很多创业公司的一样,我们选用的是阿里云的ECS+RDS。因为如果自己购买服务器,不管是运维成本还是物理成本都是比较高的。

一天将近半夜12点的时候,报警日志突然出现了MySQL server has gone away

遇到问题肯定是先Baidu,我找到了MySQL官方的解释,原因是查询的时候,出现的mysql断开的情况。我登录阿里云rds后台,发现wait_timeout时间长得很。不应该会出现超时的情况。

一个同事:“会不会和rds经常CPU报警有关?”

我勒个去,我查了一下rds监控,果然CPU持续升高。

问题跟进

rds自带了日志系统,可以方便。查看了一下慢日志系统,果然有很多的慢SQL日志。

我曹,每次扫描了8W多行。看来是没有使用到索引。加上次数频繁,解析的总次数高达 1762833762 行。

定位问题

查看MySQL的执行计划

mysql> explain extended SELECT * FROM `test` WHERE `is_deleted` =0 AND `a` = 81644;
+----+-------------+-----------+------+---------------+------+---------+------+-------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+-------+-------------+
|  1 | SIMPLE      | test | ALL  | a_index      | NULL | NULL    | NULL | 86172 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+-------+-------------+
1 row in set, 3 warnings (0.01 sec)
mysql> show warnings;
| Level   | Code | Message                                                                                             
| Warning | 1739 | Cannot use ref access on index 'a_index' due to type or collation conversion on field 'a'                                                                                                 |
| Warning | 1739 | Cannot use range access on index 'a_index' due to type or collation conversion on field 'a'

果然是没有用到索引,全表扫描。

原来,由于a数据类型是varchar类型的。但是查询的时候,使用的int类型,在执行SQL语句的时候,由于类型原因,造成了隐式转换。没有用到索引。所以实际上,应该把原来的SQL语句更改成 SELECT * FROM test WHERE is_deleted =0 AND a = '81644'

虽然原因找到了,但是查询的SQL那么多,定位那具体的php文件以及对于的代码行数,也是一个难题。

PHP慢日志

为了能够定位代码的效率,PHP自带一个功能,那就是慢日志。如果PHP脚本,执行时间比较长的时候,那么PHP会认为这段代码是有问题的,PHP会把代码的基本信息打印到慢日志里面,能够方便开发者定位问题。

这么说来,如果找到慢日志里面关于执行这个SQL的代码,也就能够准确定位到对应的PHP文件。

索引对性能的影响!

接下来用对比图来比较下使用索引和没有使用索引的对比吧

优化之后的SQL执行效率,相比之前要高出很多,CPU占用率稳定保持在个位数,甚至 5%一下,相比之前80%左右,呈现指数的翻倍。

总结

其实隐式转换是MySQL索引经常遇到的问题。我最开始听说是前段时间,阿里云组织了一个慢SQL的优化大赛。虽然没有得到名次,但是确实通过大赛,学到了很多关于索引的知识。

awk命令的简单介绍

背景

awk算是Linux上面比较实用频繁的命令之一。第一次见到这个命令,是同事们分析一些日志实用,通过这个命令与其他命令结合,可以有效的分析nginx日志的一些访问情况。所以我也特意找了一些资料,查询了一下。

语法规则

awk的命令的语法规则是 awk '条件类型1{动作1} 条件类型2{动作2} ...' 文件名; 。awk条件类型后面的{}是满足条件后处理的一些动作。这些动作可以形成一套连续的操作。awk的处理单元是每一行。也就是每行处理之后,再对下一行进行处理。所以,awk并不适合对大量数据处理。

awk的处理原理

feilongdeMBP:~ feilong$ awk '{print $0}' /etc/passwd
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by
# Open Directory.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
_uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico
_taskgated:*:13:13:Task Gate Daemon:/var/empty:/usr/bin/false
_networkd:*:24:24:Network Services:/var/networkd:/usr/bin/false
_installassistant:*:25:25:Install Assistant:/var/empty:/usr/bin/false
_lp:*:26:26:Printing Services:/var/spool/cups:/usr/bin/false
.....

我们发现,这样输出的内容和执行cat /etc/passwd内容是一样的。

feilongdeMBP:~ feilong$ awk '{print $1}' /etc/passwd
##
#
#
#
#
#
#
#
#
##
nobody:*:-2:-2:Unprivileged
root:*:0:0:System
daemon:*:1:1:System
_uucp:*:4:4:Unix
_taskgated:*:13:13:Task
_networkd:*:24:24:Network
_installassistant:*:25:25:Install
_lp:*:26:26:Printing
_postfix:*:27:27:Postfix
_scsd:*:31:31:Service
_ces:*:32:32:Certificate
_mcxalr:*:54:54:MCX
_appleevents:*:55:55:AppleEvents
_geod:*:56:56:Geo
_serialnumberd:*:58:58:Serial
_devdocs:*:59:59:Developer
_sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false
_mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false
.....

随着print后面的变化,输出的内容也发生了变化

所以,awk的原理是这样的

除了这个,awk还有一些标量的含义

标量 含义
NR 当前的行号
NF 每一行拥有的字段总数
FS 每行的字段分隔符(默认空格)
RS 每行的结束符(默认\n)

实际操作

以分号进行分割
feilongdeMBP:~ feilong$ awk 'FS=":" {print $1}' /etc/passwd ## 或 awk -F ":" '{print $1}' /etc/passwd
##
# User Database
#
# Note that this file is consulted directly only when the system is running
# in single-user mode.  At other times this information is provided by
# Open Directory.
#
# See the opendirectoryd(8) man page for additional information about
# Open Directory.
##
nobody
root
daemon
_uucp
_taskgated
_networkd
_installassistant
_lp
_postfix
_scsd
_ces
_mcxalr
....
比如,只看 第20行到30行的内容
feilongdeMBP:~ feilong$ awk '{if(NR>=20 && NR<=30) {print "行号是: " NR " " $0}}' /etc/passwd
行号是: 20 _scsd:*:31:31:Service Configuration Service:/var/empty:/usr/bin/false
行号是: 21 _ces:*:32:32:Certificate Enrollment Service:/var/empty:/usr/bin/false
行号是: 22 _mcxalr:*:54:54:MCX AppLaunch:/var/empty:/usr/bin/false
行号是: 23 _appleevents:*:55:55:AppleEvents Daemon:/var/empty:/usr/bin/false
行号是: 24 _geod:*:56:56:Geo Services Daemon:/var/db/geod:/usr/bin/false
行号是: 25 _serialnumberd:*:58:58:Serial Number Daemon:/var/empty:/usr/bin/false
行号是: 26 _devdocs:*:59:59:Developer Documentation:/var/empty:/usr/bin/false
行号是: 27 _sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false
行号是: 28 _mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false
行号是: 29 _ard:*:67:67:Apple Remote Desktop:/var/empty:/usr/bin/false
行号是: 30 _www:*:70:70:World Wide Web Server:/Library/WebServer:/usr/bin/false
已知 test.txt 内容是 “I am Poe,my qq is 33794712″。过滤相应字符串,是输出结果为 “Poe 33794712”
feilongdeMBP:~ feilong$ awk -F "[ ,]+" '{print $3 " " $7}' test.txt
Poe 33794712

BEGIN和END模块

begin和end主要是只在awk执行开始(还没对第一行进行操作)和结束(对最后一行处理结束)后的行为。所以,begin和end只会操作一次。所以begin和end更像是 编程语言中的默认构造函数和析构函数。

统计用户的数量
feilongdeMBP:Downloads feilong$ awk 'BEGIN{count = 0} {if (NR > 10) { count ++} } { if (NR > 10 ) { print $1}} END{print "总的用户数量是: " count}' /etc/passwd ## 以为我的机器上面前10行不是用户的数据
nobody:*:-2:-2:Unprivileged
root:*:0:0:System
daemon:*:1:1:System
_uucp:*:4:4:Unix
_taskgated:*:13:13:Task
_networkd:*:24:24:Network
_installassistant:*:25:25:Install
_lp:*:26:26:Printing
_postfix:*:27:27:Postfix
_scsd:*:31:31:Service
_ces:*:32:32:Certificate
_mcxalr:*:54:54:MCX
_appleevents:*:55:55:AppleEvents
_geod:*:56:56:Geo
_serialnumberd:*:58:58:Serial
_devdocs:*:59:59:Developer
_sandbox:*:60:60:Seatbelt:/var/empty:/usr/bin/false
_mdnsresponder:*:65:65:mDNSResponder:/var/empty:/usr/bin/false
_ard:*:67:67:Apple
_www:*:70:70:World
_eppc:*:71:71:Apple
总的用户数量是: 93
总计金额
feilongdeMBP:~ feilong$ cat test.txt
Name    1st 2st 3st
Tyler   100 200 500
Start   59  30  444
Jack    345 222 67

feilongdeMBP:~ feilong$  awk 'BEGIN{ totle = 0;} NR==1{print "Name\t1st\t2st\t3st\tTotle"} NR>=2{totle = $2 + $3 + $4; print $1 "\t" $2 "\t" $3 "\t" $4 "\t"  totle}' test.txt
Name    1st 2st 3st Totle
Tyler   100 200 500 800
Start   59  30  444 533
Jack    345 222 67  634
统计字节数量
feilongdeMBP:~ feilong$ ll | grep JPG
-rw-r--r--@   1 feilong  access_bpf    255800  3  1 16:49 IMG_0898.JPG
-rw-r--r--@   1 feilong  access_bpf    258234  3  1 16:49 IMG_0899.JPG
-rw-r--r--@   1 feilong  access_bpf    338363  3  4 10:32 IMG_0930.JPG

feilongdeMBP:~ feilong$ ll | grep JPG | awk 'BEGIN{size = 0;} {size += $5} END{print "The .JPG file size:" size/1024/1024 "MB"}'
The .JPG file size:0.812909MB

awk还有丰富的运算符

awk支持大多数的运算符,这些运算符和编程语言基本类似

正则表达式

语法结构 awk '/正则表达式/{动作}' 文件

找出匹配包含root的行
feilongdeMBP:~ feilong$ awk '/root/{print $0}' /etc/passwd
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
_cvmsroot:*:212:212:CVMS Root:/var/empty:/usr/bin/false

其他

awk还有其他的功能,比如支持for循环,if语句,while循环等待

for 循环
feilongdeMBP:~ feilong$ awk '/root/{print $0; for(i=1; i< 4; i++) {print "test"}}' /etc/passwd
root:*:0:0:System Administrator:/var/root:/bin/sh
test
test
test
daemon:*:1:1:System Services:/var/root:/usr/bin/false
test
test
test
_cvmsroot:*:212:212:CVMS Root:/var/empty:/usr/bin/false
test
test
test

参考资料

Linux自定义PHP的环境变量

很多时候,我们会使用 PHP的$_SERVER数组,通过这个数组,可以获取一些服务器的变量信息。但是不同的模式下,这个全局数组是不一样的。比如,在web模式下,$_SERVER 是获取的fastcgi_params,在cli模式下,获取的是环境变量(也就是常见的Linux 的export设置的)

举个例子,我们要设置$_SERVER[‘AAAAA’]=’test_data’

刚开始,不管web模式下,还是cli模式下,都是没有这个值的。

web模式

cli模式

更改nginx 的环境变量

找到fastcgi_params文件,一般是和nginx.conf在同一个目录,

$ sudo nginx -s reload 

然后刷新页面

更改cli模式先的环境变量

$ vim ~/.bashrc

$ source ~/.bashrc
$ php -r 'var_dump($_SERVER["AAAAA"]);';

一次由于curl调用https接口导致的502

背景描述

最近在调研百度人脸识别的服务,百度的人脸识别是免费的,但是有QPS的限制,QPS免费的最大值是5,也就是峰值在每秒5次,都是可以免费使用的。我从百度平台下载的SDK,但是到了第一步就被卡主了。

当我使用检测接口的时候,频繁出现 502。在之前,本地运行PHP的时候,也会偶尔出现502,但是并没有这么高。这次变态的浮现率是100%.

查了下nginx日志*173 upstream prematurely closed connection while reading response header from upstream, client: 127.0.0.1, server: sdeno-api, request: "GET /facev1/test HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000"

在网上查了一下,说什么的都有,大多数是在说nginx的问题。没有一个能够解决问题。

发现问题

我发现,出现http 502 的接口使用了curl,所以很可能是curl代码有问题。我自己有写了一段代码,调用百度首页http://www.baidu.com,咦?正常返回啊。完全没有问题。后来百度查了很多,最终发现一个不起眼的问题 php curl 调用https出现502问题。我验证了一些,我曹,果然只有https接口是不正常的。

解决办法

后来找到了一个靠谱的方法,那就是使用sudo重启php-fpm.

$ brew services stop php53
$ sudo brew services start php53

其实除了这样,还可以考虑下重装或者升级PHP的版本。我把本地的php5.3卸载了,重新安装了php5.6也解决了问题。

$ brew uninstall php53
$ brew install php56

关于limit_req和limit_conn的区别

request和connection区别

在nginx里面,limit_req和limit_conn都是用来限流的但是两者不在一个层次上,在此之前,需要先清楚request和connect的区别。
request是请求,是http层面的。connection是链接,指的是tcp层面。所以,从含义上面可以知道两者不在一个层次。
我们在打开一个网页的时候,请求过程一般就是先经过tcp三次握手,然后在进行http请求。也就是一个connection建立之后,可以有很多request。

一个connection建立,只要服务器处理的过来,可以处理任意多的request都是没有问题的

好了,现在知道区别了。

limit_conn

http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_conn one 1; #最多可以进行1个connection
    client_body_buffer_size 256M;
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$request_filename"';
    sendfile        on;

    keepalive_timeout  65;
    include servers/*.conf;
}

上面的配置文件的含义很明白,nginx只接受最多一个connect,我们使用ab命令查看下

$ ab -n10  http://sdeno-api/info/php #默认通过一个connect送10个request

从截图看来,在一个并发下,处理10个request下完全没有问题

接下来我们使用2个并发,2个请求试下,也就是两个用户,每个用户发送一个request

$ ab -n2 -c2 http://sdeno-api/info/php

从截图中可以看到,由于nginx设置了至多限制一个并发,所以导致两个用户的请求只能有一个被处理掉,另外一个返回http 503

limit_req

下面更改下nginx.conf配置文件

http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_req_zone $binary_remote_addr zone=req_one:20m rate=1r/s;
    limit_conn one 20;
    limit_req zone=req_one burst=5;
    client_body_buffer_size 256M;
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$request_filename"';
    sendfile        on;

    keepalive_timeout  65;
    include servers/*.conf;
}

上面的配置的含义是请求速率限制为1r/s,然后再缓存5个request,然后再依次处理请求(令牌桶算法)

$ ab -n10 -c10 http://sdeno-api/info/php

我们使用压测命令,10个并发,10个请求,根据配置的文件,当有请求过来,nginx先处理一个请求,然后将5个请求缓存下来(burst=5),根据设置的速率1r/s进行处理,也就是一共能够处理6个请求,其余的请求则被丢掉。

接下来我们再继续更改下nginx配置文件

$ ab -n10 -c10 http://sdeno-api/info/php
http {
    limit_conn_zone $binary_remote_addr zone=one:20m;
    limit_req_zone $binary_remote_addr zone=req_one:20m rate=1r/s;
    limit_conn one 20;
    limit_req zone=req_one burst=5 nodelay;
    client_body_buffer_size 256M;
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for" "$request_filename"';
    sendfile        on;

    keepalive_timeout  65;
    include servers/*.conf;
}

咦?增加了nodelay好像和不加有点不同。这是因为请求过来的时候,会爆发出一个峰值的处理能力,处理的总的请求数是burst+1,其余的请求丢弃。

总结

  • request和connect是完全不同层面的含义,一个属于http,一个属于tcp

  • limit_req zone=req_zone burst=5 依照在limti_req_zone中配置的rate来处理请求,同时设置了一个大小为5的缓冲队列,在缓冲队列中的请求会等待慢慢处理,超过了burst缓冲队列长度和rate处理能力的请求被直接丢弃,表现为对收到的请求有延时

  • limit_req zone=req_zone burst=5 nodelay 依照在limti_req_zone中配置的rate来处理请求,同时设置了一个大小为5的缓冲队列,当请求到来时,会爆发出一个峰值处理能力,对于峰值处理数量之外的请求,直接丢弃。

参考文献