使用 deb 包安装 docker

手头有一个某云平台的虚拟机,很不幸无法使用docker官方的脚本安装docker:

curl -sSL https://get.docker.com/ | sh
usermod -aG docker $USER
systemctl enable docker
systemctl start docker

报错则是xxx链接超时。我使用的是debian系统,所以这一篇记录如何使用deb包安装docker。

查看系统版本

YUKI.N > lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.11 (stretch)
Release:        9.11
Codename:       stretch

进入下载页

进入到下载包页面 https://download.docker.com/linux/

点击进入 debian>dists>stretch 进入了这个连接地址 https://download.docker.com/linux/debian/dists/

选择一个比较新的版本

我选择的是 19.03

wget https://download.docker.com/linux/debian/dists/stretch/pool/stable/amd64/docker-ce_19.03.0~3-0~debian-stretch_amd64.deb

安装命令

sudo dpkg -i docker-ce*.deb
sudo apt-get -f install

安装完成后查看版本信息:

YUKI.N > docker version
Client: Docker Engine - Community
 Version:           19.03.2
 API version:       1.40
 Go version:        go1.12.8
 Git commit:        6a30dfca03
 Built:             Thu Aug 29 05:29:49 2019
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.0
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.5
  Git commit:       aeac9490dc
  Built:            Wed Jul 17 18:12:33 2019
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

至此安装完成。


laravel 根据 postgresql jsonb 字段数组筛选数据行

这篇文章记录一下我如何解决在 laravel 中使用jsonb 数组筛选数据行。

背景

先描述一下背景,数据库中我们有一个jsonb字段data,这个字段里存的是数组。在 laravel 数据表中定义如下:

        Schema::create('xxx', function (Blueprint $table) {
            $table->uuid('uuid');
            $table->jsonb("data")->nullable(); // 
            ... ...
        });

在类中定义如下:


    protected $casts = [
        'data' => 'array',
    ];

在data中我们使用数组存了一组无序的数据,例如:

[ a,b,c ]

初始想法

这种情况和我们使用 jsonb 存对象,根据对象取记录行是不一样的。

先来看下常规的json对象如何筛选记录行的。如果我们使用对象来存,可以很方便的达到我们的效果,例如:

if (XxxClass::where("data->id", $id)->where("data->name", $name)->count() == 0) {
}

数组无法使用这种方式取值,故而pass。

而laravel我也没有查到相关的办法解决。所以比较好的办法是自己写SQL语句查询。参考 postgresql 官方的文档:https://www.postgresql.org/docs/9.4/functions-json.html

很自然的我选择了 ?| 操作符进行筛选,并且成功了!

p1

具体 SQL 语句如下:

select * from "xxx" where "data" :: jsonb ?| ARRAY['b']

由此查到了相关的记录。

p1

困难初现

难题在于通过这种方式无法在 laravel 中使用:

p1

以下是错误提示:

p1

从错误提示里可以知道 被转义成了laravel eloquent 默认的 变量了。 但即使使用了转义符 \ 仍然是同样的错误。

为了更准确地定位问题,我用了下面的代码查看生成的sql语句是什么:

echo DB::select('select * from "xxx" where "data" :: jsonb ?| ARRAY[\'b\']')->toSql();

这个和我使用纯sql语句一毛一样。在此时我一度陷入了困境,便开始群聊和谷歌之旅。

定位问题

此时一位能力强悍的老同事,找到了一个issue,不过竟然是golang的框架 gorm 的讨论:Way to escape Question Mark in Raw Query? #533 - github

问题也就是:这是 postgresql 9.4 的一个系统bug,目前官方并没有解决办法。

绕过问题

虽然问题一时半会无法解决,但是社区里找到了绕过此问题的方法,那就是——不使用 ?| 操作符!

https://laravel.io/forum/01-25-2015-postgres-94-new-question-mark-operator-cant-be-used-in-eloquent-raw-queries

具体来说,是使用 @> 操作符替代 ?|操作符:

select * from "xxx" where "data" :: jsonb @> '["b"]'

至于这两个操作符具体实现和效率有何不同,就不太清楚了,不过已经能解决目前遇到的问题了。转换到 laravel 的实现,使用下面的代码:

DB::table('xxx')->whereRaw('"data" :: jsonb @> \'["'.$this->name.'"]\';')->get()

问题解决~


laravel 中如何写json文件

写json要注意使用如下方法:

$content = stripslashes(json_encode($array));

stripslashes用于删除反斜杠。

写文件使用laravel方法

  • Storage::put($file, $content);

或php原生方法:

  • file_put_contents
    // 读文件

    $jsonString = file_get_contents(base_path('resources/lang/en.json'));

    $data = json_decode($jsonString, true);

    // 更新内容

    $data['country.title'] = "Change Manage Country";

    // 写文件

    $newJsonString = json_encode($data, JSON_PRETTY_PRINT);

    file_put_contents(base_path('resources/lang/en.json'), stripslashes($newJsonString));

参考资料


PHP json_encode 转换空数组为对象

今天使用 json_encode() 将数组转换成json格式时,发现期望显示空对象的json,被转换成了空数组。这样一来和外部交互时就出了问题,数据结构不一致,导致对端解析json失败。

原本:

$server = [
    "stats" => [],
    "api" => [
        "tag" => "api",
        "services" => [
            "StatsService"
        ]
    ],
 ]

转换为json:

{"stats":[],"api":{"tag":"api","services":["StatsService"]}}

而我们期望的json为:

{"stats":{},"api":{"tag":"api","services":["StatsService"]}}

使用 ArrayObject 可以解决这个问题。按如下方式定义数组:

$server = [
    "stats" => new \ArrayObject(),
    "api" => [
        "tag" => "api",
        "services" => [
            "StatsService"
        ]
    ],
 ]

即可得到期望的json。


简易 tcp 流量转发

tcp 流量转发有很多方式可以达成,比如系统默认iptables就可以做到,又比如可以用haproxy完成。这篇文章记录的都不是这两个。

这里我介绍一个简单的200行左右的c代码,完成的端口转发这样的功能。

GitHub地址:https://github.com/bovine/datapipe

  1. 下载c文件:

    wget https://raw.githubusercontent.com/bovine/datapipe/master/datapipe.c
    
  2. 编译:

    前提是已经安装了gcc编译工具。

    gcc datapipe.c -o dp
    
  3. 赋予可执行权限:

    chmod +x dp
    
  4. 运行示例:

    ./dp 0.0.0.0 40022 47.96.79.77 40022
    

    在这里吧40022的所有端口流量转到 47.96.79.77:40022

源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#if defined(__WIN32__) || defined(WIN32) || defined(_WIN32)
  #define WIN32_LEAN_AND_MEAN
  #include <winsock.h>
  #define bzero(p, l) memset(p, 0, l)
  #define bcopy(s, t, l) memmove(t, s, l)
#else
  #include <sys/time.h>
  #include <sys/types.h>
  #include <sys/socket.h>
  #include <sys/wait.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  #include <unistd.h>
  #include <netdb.h>
  #include <strings.h>
  #define recv(x,y,z,a) read(x,y,z)
  #define send(x,y,z,a) write(x,y,z)
  #define closesocket(s) close(s)
  typedef int SOCKET;
#endif

#ifndef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif

struct client_t
{
  int inuse;
  SOCKET csock, osock;
  time_t activity;
};

#define MAXCLIENTS 20
#define IDLETIMEOUT 300

const char ident[] = "$Id: datapipe.c,v 1.8 1999/01/29 01:21:54 jlawson Exp $";

int main(int argc, char *argv[])
{ 
  SOCKET lsock;
  char buf[4096];
  struct sockaddr_in laddr, oaddr;
  int i;
  struct client_t clients[MAXCLIENTS];

#if defined(__WIN32__) || defined(WIN32) || defined(_WIN32)
  /* Winsock needs additional startup activities */
  WSADATA wsadata;
  WSAStartup(MAKEWORD(1,1), &wsadata);
#endif

  /* check number of command line arguments */
  if (argc != 5) {
    fprintf(stderr,"Usage: %s localhost localport remotehost remoteport\n",argv[0]);
    return 30;
  }

  /* reset all of the client structures */
  for (i = 0; i < MAXCLIENTS; i++)
    clients[i].inuse = 0;

  /* determine the listener address and port */
  bzero(&laddr, sizeof(struct sockaddr_in));
  laddr.sin_family = AF_INET;
  laddr.sin_port = htons((unsigned short) atol(argv[2]));
  laddr.sin_addr.s_addr = inet_addr(argv[1]);
  if (!laddr.sin_port) {
    fprintf(stderr, "invalid listener port\n");
    return 20;
  }
  if (laddr.sin_addr.s_addr == INADDR_NONE) {
    struct hostent *n;
    if ((n = gethostbyname(argv[1])) == NULL) {
      perror("gethostbyname");
      return 20;
    }    
    bcopy(n->h_addr, (char *) &laddr.sin_addr, n->h_length);
  }

  /* determine the outgoing address and port */
  bzero(&oaddr, sizeof(struct sockaddr_in));
  oaddr.sin_family = AF_INET;
  oaddr.sin_port = htons((unsigned short) atol(argv[4]));
  if (!oaddr.sin_port) {
    fprintf(stderr, "invalid target port\n");
    return 25;
  }
  oaddr.sin_addr.s_addr = inet_addr(argv[3]);
  if (oaddr.sin_addr.s_addr == INADDR_NONE) {
    struct hostent *n;
    if ((n = gethostbyname(argv[3])) == NULL) {
      perror("gethostbyname");
      return 25;
    }    
    bcopy(n->h_addr, (char *) &oaddr.sin_addr, n->h_length);
  }

  /* create the listener socket */
  if ((lsock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    perror("socket");
    return 20;
  }
  if (bind(lsock, (struct sockaddr *)&laddr, sizeof(laddr))) {
    perror("bind");
    return 20;
  }
  if (listen(lsock, 5)) {
    perror("listen");
    return 20;
  }

  /* change the port in the listener struct to zero, since we will
   * use it for binding to outgoing local sockets in the future. */
  laddr.sin_port = htons(0);

  /* fork off into the background. */
#if !defined(__WIN32__) && !defined(WIN32) && !defined(_WIN32)
  if ((i = fork()) == -1) {
    perror("fork");
    return 20;
  }
  if (i > 0)
    return 0;
  setsid();
#endif

  
  /* main polling loop. */
  while (1)
  {
    fd_set fdsr;
    int maxsock;
    struct timeval tv = {1,0};
    time_t now = time(NULL);

    /* build the list of sockets to check. */
    FD_ZERO(&fdsr);
    FD_SET(lsock, &fdsr);
    maxsock = (int) lsock;
    for (i = 0; i < MAXCLIENTS; i++)
      if (clients[i].inuse) {
        FD_SET(clients[i].csock, &fdsr);
        if ((int) clients[i].csock > maxsock)
          maxsock = (int) clients[i].csock;
        FD_SET(clients[i].osock, &fdsr);
        if ((int) clients[i].osock > maxsock)
          maxsock = (int) clients[i].osock;
      }      
    if (select(maxsock + 1, &fdsr, NULL, NULL, &tv) < 0) {
      return 30;
    }

    /* check if there are new connections to accept. */
    if (FD_ISSET(lsock, &fdsr))
    {
      SOCKET csock = accept(lsock, NULL, 0);
     
      for (i = 0; i < MAXCLIENTS; i++)
        if (!clients[i].inuse) break;
      if (i < MAXCLIENTS)
      {
        /* connect a socket to the outgoing host/port */
        SOCKET osock;
        if ((osock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
          perror("socket");
          closesocket(csock);
        }
        else if (bind(osock, (struct sockaddr *)&laddr, sizeof(laddr))) {
          perror("bind");
          closesocket(csock);
          closesocket(osock);
        }
        else if (connect(osock, (struct sockaddr *)&oaddr, sizeof(oaddr))) {
          perror("connect");
          closesocket(csock);
          closesocket(osock);
        }
        else {
          clients[i].osock = osock;
          clients[i].csock = csock;
          clients[i].activity = now;
          clients[i].inuse = 1;
        }
      } else {
        fprintf(stderr, "too many clients\n");
        closesocket(csock);
      }        
    }

    /* service any client connections that have waiting data. */
    for (i = 0; i < MAXCLIENTS; i++)
    {
      int nbyt, closeneeded = 0;
      if (!clients[i].inuse) {
        continue;
      } else if (FD_ISSET(clients[i].csock, &fdsr)) {
        if ((nbyt = recv(clients[i].csock, buf, sizeof(buf), 0)) <= 0 ||
          send(clients[i].osock, buf, nbyt, 0) <= 0) closeneeded = 1;
        else clients[i].activity = now;
      } else if (FD_ISSET(clients[i].osock, &fdsr)) {
        if ((nbyt = recv(clients[i].osock, buf, sizeof(buf), 0)) <= 0 ||
          send(clients[i].csock, buf, nbyt, 0) <= 0) closeneeded = 1;
        else clients[i].activity = now;
      } else if (now - clients[i].activity > IDLETIMEOUT) {
        closeneeded = 1;
      }
      if (closeneeded) {
        closesocket(clients[i].csock);
        closesocket(clients[i].osock);
        clients[i].inuse = 0;
      }      
    }
    
  }
  return 0;
}


etcd 集群安装记录

这篇文章记录下我安装etcd集群的步骤。

假设我们要在一下三台机器安装etcd集群:

  • 10.10.10.1
  • 10.10.10.2
  • 10.10.10.3

1. 时间同步

在每台机器上运行如下命令:

apt-get install ntpdate -y
ntpdate time.windows.com

2. 生成证书

使用cfssl生成etcd集群的证书,生成的证书位于/etc/etcd/ssl目录下:

#!/usr/bin/env bash

set -e
cdir="$(cd "$(dirname "$0")" && pwd)"

mkdir -p /var/lib/etcd
mkdir -p /etc/etcd/ssl

echo "下载 cfssl"
curl -o /usr/bin/cfssl https://pkg.cfssl.org/R1.2/cfssl_linux-amd64
curl -o /usr/bin/cfssljson https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64
chmod +x /usr/bin/cfssl*

echo "CA 证书 配置: ca-config.json ca-csr.json"
cd /etc/etcd/ssl
cat >ca-config.json <<EOF
{
    "signing": {
        "default": {
            "expiry": "87600h"
        },
        "profiles": {
            "server": {
                "expiry": "87600h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth",
                    "client auth"
                ]
            },
            "client": {
                "expiry": "87600h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "client auth"
                ]
            },
            "peer": {
                "expiry": "87600h",
                "usages": [
                    "signing",
                    "key encipherment",
                    "server auth",
                    "client auth"
                ]
            }
        }
    }
}
EOF

cat >ca-csr.json <<EOF
{
    "CN": "etcd",
    "key": {
        "algo": "rsa",
        "size": 2048
    }
}
EOF

echo "生成 CA 证书:  ca.csr ca-key.pem ca.pem"
cfssl gencert -initca ca-csr.json | cfssljson -bare ca -

echo "服务器端证书配置: config.json"
cat >config.json <<EOF
{
    "CN": "etcd-0",
    "hosts": [
        "localhost",
        "10.10.10.1",
        "0.0.0.0",
        "10.10.10.2",
        "10.10.10.3"
    ],
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "C": "US",
            "L": "CA",
            "ST": "San Francisco"
        }
    ]
}
EOF
 
echo "生成服务器端证书 server.csr server.pem server-key.pem"
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server config.json | cfssljson -bare server
echo "生成peer端证书 peer.csr peer.pem peer-key.pem"
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer config.json | cfssljson -bare peer

3. etcd 默认配置文件

新建一个 etcd.conf 文件如下:

ETCD_NAME=node01
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="https://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="https://0.0.0.0:2379"

#[cluster]
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://etcd-0:2380"
ETCD_INITIAL_CLUSTER="node01=https://node01:2380,node02=https://node02:2380,node03=https://node03:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="nodecluster"
ETCD_ADVERTISE_CLIENT_URLS="https://node01:2379"
#ETCD_DISCOVERY=""
#ETCD_DISCOVERY_SRV=""
#ETCD_DISCOVERY_FALLBACK="proxy"
#ETCD_DISCOVERY_PROXY=""
#ETCD_STRICT_RECONFIG_CHECK="false"
#ETCD_AUTO_COMPACTION_RETENTION="0"
#
#[proxy]
#ETCD_PROXY="off"
#ETCD_PROXY_FAILURE_WAIT="5000"
#ETCD_PROXY_REFRESH_INTERVAL="30000"
#ETCD_PROXY_DIAL_TIMEOUT="1000"
#ETCD_PROXY_WRITE_TIMEOUT="5000"
#ETCD_PROXY_READ_TIMEOUT="0"
#
#[security]
ETCD_CERT_FILE="/app/kelu/etcd/ssl/server.pem"
ETCD_KEY_FILE="/app/kelu/etcd/ssl/server-key.pem"
ETCD_CLIENT_CERT_AUTH="true"
ETCD_TRUSTED_CA_FILE="/app/kelu/etcd/ssl/ca.pem"
ETCD_AUTO_TLS="true"
ETCD_PEER_CERT_FILE="/app/kelu/etcd/ssl/peer.pem"
ETCD_PEER_KEY_FILE="/app/kelu/etcd/ssl/peer-key.pem"
#ETCD_PEER_CLIENT_CERT_AUTH="false"
ETCD_PEER_TRUSTED_CA_FILE="/app/kelu/etcd/ssl/ca.pem"
ETCD_PEER_AUTO_TLS="true"
#
#[logging]
#ETCD_DEBUG="false"
# examples for -log-package-levels etcdserver=WARNING,security=DEBUG
#ETCD_LOG_PACKAGE_LEVELS=""
#[profiling]
#ETCD_ENABLE_PPROF="false"
#ETCD_METRICS="basic"

文件中包含了很多配置,实际上很多配置我们会在启动时将他们覆盖,所以这个文件中我们只要保持原样即可,不需要修改。

4. etcd 系统服务

手动新建/编辑文件 /etc/systemd/system/etcd.service :

[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/coreos

[Service]
Type=notify
WorkingDirectory=/app/kelu/etcddata/
EnvironmentFile=/app/kelu/etcd/etcd.conf
User=root
ExecStart=/usr/bin/etcd \
  --name node01  \
  --cert-file=/app/kelu/etcd/ssl/server.pem  \
  --key-file=/app/kelu/etcd/ssl/server-key.pem  \
  --peer-cert-file=/app/kelu/etcd/ssl/peer.pem  \
  --peer-key-file=/app/kelu/etcd/ssl/peer-key.pem  \
  --trusted-ca-file=/app/kelu/etcd/ssl/ca.pem  \
  --peer-trusted-ca-file=/app/kelu/etcd/ssl/ca.pem  \
  --initial-advertise-peer-urls https://10.10.10.1:2380  \
  --listen-peer-urls https://0.0.0.0:2380  \
  --listen-client-urls https://0.0.0.0:2379  \
  --advertise-client-urls https://10.10.10.1:2379  \
  --initial-cluster-token etcd-cluster-0  \
  --initial-cluster node01=https://10.10.10.1:2380,node02=https://10.10.10.2:2380,node03=https://10.10.10.3:2380  \
  --heartbeat-interval=1000 \
  --election-timeout=5000 \
  --initial-cluster-state new  \
  --data-dir=/app/kelu/etcddata
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

上边是10.10.10.1上的例子。

对于另外两台机器,需要修改下面几个地方:

  • 修改name为node2/node3等。
  • 将 initial-advertise-peer-urls 和advertise-client-urls改为机器IP。
  • 同时可以针对性地修改 heartbeat-interval 和 election-timeout 的配置,修改心跳监测时间和超时重新选举时间。

5. etcd 安装

准备材料都准备好了,使用下面脚本,在三台机器上依次运行:

#!/usr/bin/env bash

set -e

rm -rf /app/kelu/etcd /app/kelu/etcddata
mkdir -p /app/kelu/etcddata /app/kelu/etcd 

##### 将第3步的etcd.conf 拷贝到目的目录
cp etcd.conf /app/kelu/etcd 
cp -R /etc/etcd/ssl /app/kelu/etcd

if [ ! -e etcd-v3.1.18-linux-amd64.tar.gz ]; then
  wget https://github.com/coreos/etcd/releases/download/v3.1.18/etcd-v3.1.18-linux-amd64.tar.gz
fi

tar -zxvf etcd-v3.1.18-linux-amd64.tar.gz > /dev/null

mv etcd-v3.1.18-linux-amd64 /app/kelu/etcd

rm -rf /usr/bin/etcd /usr/bin/etcdctl /etc/systemd/system/etcd.service
ln -s /app/kelu/etcd/etcd-v3.1.18-linux-amd64/etcd /usr/bin/etcd
ln -s /app/kelu/etcd/etcd-v3.1.18-linux-amd64/etcdctl /usr/bin/etcdctl

##### 将第4步的 etcd.service 拷贝到系统目录
cp -f cfg/etcd.service.$1 /etc/systemd/system/etcd.service

systemctl daemon-reload
systemctl enable etcd
systemctl start etcd
systemctl status etcd

6. etcd 检查

安装完成后通过下面的命令进行验证:

etcdctl \
  --ca-file=/app/kelu/etcd/ssl/ca.pem \
  --cert-file=/app/kelu/etcd/ssl/peer.pem \
  --key-file=/app/kelu/etcd/ssl/peer-key.pem \
  --endpoints=https://10.10.10.1:2379,https://10.10.10.2:2379,https://10.10.10.3:2379  \
  cluster-health

如果正常运行,将会有healthy等输出显示。