2019年9月

一、下载安装

mariadb是属于mysql的一个分支,是其创始人在mysql被卖给oracle之后重新分出来的,maria取自于他女儿的名字。mariadb完全兼容于mysql,在很多新版本的linux系统中,mysql都已经被替换成了mariadb。

mariadb的官网:mariadb官网,下载地址:下载地址。最新稳定版本的下载直链为:

wget https://downloads.mariadb.com/MariaDB/mariadb-10.5.0/bintar-linux-systemd-x86_64/mariadb-10.5.0-linux-systemd-x86_64.tar.gz

首先把安装包下载到本地,然后解压到/usr/local目录:

tar -zxvf mariadb-10.5.0-linux-systemd-x86_64.tar.gz  -C /usr/local/
ln -s /usr/local/mariadb-10.5.0-linux-systemd-x86_64/ /usr/local/mysql

初始化数据库,设定数据存储目录为/appdata/mysql,启动用户为mysql

# 创建mysql用户
useradd -s /sbin/nologin -M mysql
# 创建数据库文件夹
mkdir /appdata/mysql -p
# 初始化数据库
/usr/local/mysql/scripts/mysql_install_db \
    --basedir=/usr/local/mysql \
    --datadir=/appdata/mysql \
    --user=mysql

初始化数据库的过程中如果报错:

> Installing MariaDB/MySQL system tables in '/xxx/mariadb' ...
/usr/local/mariadb/bin/mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file: No such file or directory
 
Installation of system tables failed!  Examine the logs in
/udata/mariadb for more information.
 
...

说明系统缺少组件库libaio,需要安装手动安装:

# centos
yum install libaio libaio-devel
# ubuntu
apt install libaio1

执行成功后输出:

Installing MariaDB/MySQL system tables in '/appdata/mysql' ...
OK

To start mysqld at boot time you have to copy
support-files/mysql.server to the right place for your system

...

到这里数据库就已经安装完成了,接下来要做的就是配置。

二、配置

修改/etc/mysql/my.cnf,设置pid/socket/log等文件的路径,把它们统一存到/appdata/mysql/run/下:

[mysqld]
datadir=/appdata/mysql
socket=/appdata/mysql/run/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
# Settings user and group are ignored when systemd is used.
# If you need to run mysqld under a different user or group,
# customize your systemd unit file for mariadb according to the
# instructions in http://fedoraproject.org/wiki/Systemd

[mysqld_safe]
log-error=/appdata/mysql/run/mysql.log
pid-file=/appdata/mysql/run/mysql.pid

[mysql]
socket=/appdata/mysql/run/mysql.sock

[mysqladmin]
socket=/appdata/mysql/run/mysql.sock

#
# include all files from the config directory
#
!includedir /etc/my.cnf.d

注意:

  1. /appdata/mysql/run目录要提前创建
  2. 如果修改了socket的路径,还要修改[mysql]和[mysqladmin]段的socket路径,要和[mysqld]中的socket路径一致

设置路径权限:

chown mysql.mysql -R /usr/local/mysql /appdata/mysql

添加mysql命令到系统路径,修改/etc/profile文件:

MYSQL_HOME=/usr/local/mysql
export PATH=$PATH:$MYSQL_HOME/bin

修改后source /etc/profile生效。

三、添加系统服务

3.1 service系统服务

对于使用service命令启动的服务,复制mysql主目录下的support/mysql.server文件到/etc/init.d/

cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
chmod +x /etc/init.d/mysqld

然后修改文件中的配置:

basedir=/usr/local/mysql # 安装目录
datadir=/appdata/mysql # 数据目录
mysqld_pid_file_path=/appdata/mysql/run/mysql.pid # pid文件目录
注意:配置要和上面my.cnf中的配置一一对应

启动:service mysqld start

添加到开机启动:

chkconfig --add mysqld
chkconfig mysqld on

3.2 systemd系统服务

systemd服务的文件在安装路径/support-files/systemd/mariadb.service

cp support-files/systemd/mariadb.service /etc/systemd/system/mysqld.service

复制完后执行systemctl start mysqld启动服务,然后设置开机启动:

systemctl enable mysqld

四、设置root用户密码

系统服务起来后,可以使用mysqladmin初始化root用户的密码:

mysqladmin -u root password '123456'

如果出现:

image.png

说明没有没有权限登录,需要通过安全模式启动mysql来修改root密码,在my.cnf中添加以下内容:

[mysqld]
skip-grant-tables
skip-networking

然后重启服务,使用root身份登录(不用密码),执行以下命令修改密码:

use mysql;
# 刷新权限
flush privileges;
# 设置密码
set password for 'root'@'localhost' = password('123456');
# 刷新权限
flush privileges;

如果执行命令的时候出现报错:

ERROR 1290 (HY000): The MariaDB server is running with the --skip-grant-tables option so it cannot execute this statement

说明安全模式下的权限还没有更新,要先刷新一下权限才行:

flush privileges;

修改完成后去掉my.cnf中添加的参数,重启服务,使用上面设置的密码登陆就可以了:

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 11
Server version: 10.4.8-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> 

一、刷breed

breed是一个专门用于刷固件的后台,支持对系统当前的数据进行操作。breed是刷固件最重要的一步,K2系统默认的版本已经做了限制,不能直接刷上breed,需要在网上找教程刷。

我手里的这个路由器原来刷过,因此这里略过这一步。

进去breed的方法

  1. 拔掉路由器电源,按住重置键,然后插上电源线,通电后10秒钟左右松开重置键。
  2. PC通过网线直连路由器,访问http://192.168.1.1

breed首页:

image72bde19e2f98c417.png

二、下载固件

相关资源地址:

下载固件在百度网盘下载,K2的固件名字是:RT-AC54U-GPIO-1-PSG1218-64M,不要下错了。

三、刷入固件

进入breed,固件更新页面选择下载的固件,然后点击上传(注意勾上自动重启选项):

imagedfbeb6a99ac68291.png

确认固件信息,主要是对比MD5是否正确,原始的MD5在百度网盘中有下载。确认无误后点击更新开始刷固件:

imagef959b6c3faaa4d55.png

刷固件需要1-2分钟时间,耐心等待,不要断开电源:

image9da269ad0e96538f.png

刷入完成后,设备自动重启,等待一两分钟系统启动:

imagec4088814141c788a.png

Padavan固件的首页是192.168.123.1,默认密码是admin/admin,启动后在浏览器直接访问:

imagec25d77c20d6085ba.png

大功告成!

四、其他

一、缓存

缓存的作用一般是为了减轻数据库压力设计的,因为数据都是读写磁盘,当并发量大的时候,磁盘IO可能跟不上并发量。而缓存一般设计都是放在内存中的,最常见的例如redis和memcached,都是把数据都缓存在内存中。读写内存的速度比磁盘快很多,因此把常用的数据都放到内存中做缓存可以给数据库减轻很大的压力。

一个正常的缓存处理流程为:

  1. 先判断缓存中是否存在数据,如果缓存存在数据,直接读缓存中的数据。
  2. 如果缓存不存在,从数据库读数据。
  3. 如果数据库中存在数据,返回数据库中的数据并写到缓存。
  4. 如果数据库中也不存在数据,直接返回。

image2db4e44cb2632283.png

而作为缓存系统,需要考虑的几个问题是:缓存穿透、缓存击穿和缓存雪崩。

二、缓存穿透

缓存穿透:查询一个数据库不存在的数据,因为数据都不存在,所以缓存中也不存在,请求会直接去数据库中查询,这种情况就产生了缓存穿透。

例如对一个只含非负整数的表中查询id = -1的记录,缓存中不存在数据,所以每次都会请求数据库。当有攻击者恶意使用这种方式频繁请求数据时,会给后端数据库造成极大的压力。

解决方案:

  1. 增加数据校验,直接过滤掉明显不存在的数据。这里一般采用布隆过滤器,预先生成所有可能的数据的bitmap,请求数据库前先在过滤器中查找,过滤掉不可能的数据。
  2. 对不存在的值也设置缓存,在数据库中查询到不存在的数据之后在缓存中添加一个key=null的值。

三、缓存击穿

缓存击穿:某个key缓存在某一时刻失效,由于请求量特别大,导致这一时刻的所有请求同时都直接请求数据库,给数据库带来巨大压力。

解决方案:使用互斥锁或者其它方法锁住这个key(单例模式),当有请求已经在查询这个key时,等待请求,这个请求完成后数据写入缓存再从缓存中读取数据。

四、缓存雪崩

缓存雪崩:给一些键值设置了过期时间,同一时刻这些数据同时失效,请求直接到达数据库。

解决方案:

  1. 和缓存击穿一样,使请求单例执行。
  2. 不要给缓存的过期时间都设置成一样的,设置超时事件的时候加上一个随机时间作为时间。

缓存雪崩和击穿其实一样,缓存击穿时缓存雪崩的一个特例。

两者的区别:缓存雪崩是指多个缓存数据同时失效,而缓存击穿特指某一个数据失效。

一、证书相关

查看证书详细信息:

openssl x509 -in cert.pem -noout -text

查看证书所有者:

> openssl x509 -in cert.pem -noout -subject
subject= /CN=www.dyxmq.cn

打印指纹信息:

> openssl x509 -in cert.pem -noout -fingerprint
SHA1 Fingerprint=7B:29:CE:09:28:4B:38:82:DF:53:14:B0:15:00:03:66:2B:C0:90:9E

转换证书格式:

> openssl x509 -in cert.pem -inform PEM -out cert.der -outform DER

二、测试指令

测试openssl库功能,已ssl3协议连接baidu.com的443端口:

openssl s_client -connect baidu.com:443 -ssl3

一、问题现象

磁盘下有个大文件占满了空间,删除后发现文件不见了,但是磁盘空间并没有释放。

问题原因

文件还被其他进程占用,调用rm后实际上还并没有真正删除,只有当文件解除占用后(引用计数变为0)才会释放掉这部分空间。

解决方案

使用lsof命令查看文件还在被哪个进程占用的,关闭进程接触解除占用:

lsof | grep delete

过滤delete表示显示出文件已经被删除了,但是实际还被进程占有着没释放的文件。

二、场景模拟

通过以下代码模拟出进程占用文件场景,代码的作用是打开文件,然后睡眠100秒,最后结束退出

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int fd;

    if (argc < 2) {
        printf("Usage: %s file\n", argv[0]);
        return 0;
    }

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open error");
        return -1;
    }

    sleep(100);
    close(fd);

    return 0;
}

编译,并在当前目录生成一个1G左右的文件用作测试:

> gcc test.c # 编译程序
> df -h /data # 当前data分区使用了12G,剩余7.5G
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        20G   12G  7.5G  60% /data
> dd if=/dev/zero of=testfile bs=1M count=1024 # 生成一个1G左右的文件
1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 4.7793 s, 225 MB/s
> df -h /data # 目前磁盘使用了13G,剩余6.5G
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        20G   13G  6.5G  66% /data

然后执行程序,程序会卡住并睡眠100S。删除测试文件,查看磁盘空间发现没有被归还:

使用lsof命令查看文件还在被占用,kill掉之后文件解除占用,空间被归还:

一、关于glog库

golang中的glog库是google著名开源C++日志库glog的golang版本,在golang默认日志库的基础上做了更进一层的封装使得该库能更贴近日常使用。项目首页为golang/glog,当前版本的glog有以下几个特点:

  1. 支持四种日志级别:INFO < WARNING < ERROR < FATAL,支持不同级别的日志打印到不同文件中。
  2. 默认情况下日志不是打印到标准输出或标准错误,需要手动添加参数才能把日志打到标准错误中。
  3. 支持根据文件大小来切割文件,但是不支持根据日期切割。
  4. 日志输出的格式固定且不可修改:Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg,其中L表示日志级别首字符,如Info日志,L打印出来是ID`。
  5. 源码实现简单,方便自主修改。

安装glog:

go get github.com/golang/glog

1.1 基本用法

glog依赖命令行参数输入,默认是打印到文件的,在使用该库前必须先使用flag包解析命令行参数:

flag.Parse()

因为glog底层实现是先把日志输入到缓冲区buffer中,然后定时写入文件,所以在执行完日志输出后要手动执行glog.Flush()确保日志刷新到文件。一般在函数开始处执行:

defer glog.Flush()

输出日志时,需要加上-log_dir参数以指定目录写入日志文件,如以下代码:

package main

import (
    "flag"
    "github.com/golang/glog"
)

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.Info("HelloWorld")
}

编译后执行:

./glog -log_dir=log

在log目录下会生成相应的日志文件,查看对应的日志:

打印到标准输出

默认情况下,日志不是打印到标准输出中,如需打印到标准输出可以使用以下两个参数:

  1. logtostderr:打印到标准错误而不是文件。
  2. alsologtostderr:同时打印到标准错误。

这两个参数都会把日志打印到标准错误中(在linux终端环境下,前台显示的标准错误和标准输出可以认为是同一个输出),调试日志时可以加上这两个参数中的任一:

二、日志级别

v levelv module功能是最常用到的功能,适用于给同一套代码在不同环境下设置不同日志级别的功能。如在调试环境下打印出更多级别的日志,生产环境下打印更少的日志。

2.1 打印特定级别的日志

使用方法:

glog.V(n).Info("Log message")

其中n表示的是日志级别,当n小于等于设定日志级别时将会被打印。默认情况下日志级别时0,启动时通过-v=x参数设定日志级别。如在代码中分别以级别3和级别5打印两条日志:

func main() {
    flag.Parse()
    defer glog.Flush()

    glog.V(3).Info("Level 3 log")
    glog.V(5).Info("Level 5 log")
}

执行时设定日志级别为4,日志将不会打印出第二条级别为5的日志:

./glog -v=4 -log_dir=log

日志:

Log file created at: 2019/09/08 18:04:55
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:04:55.924127     546 main.go:12] Level 3 log

2.2 vmodule选项

vmodule选项可以在上面设定v level之后,单独再给某个文件设置日志级别,适用于以下场景:

  • 系统默认日志级别为3,但是希望main.go中的日志级别为5

执行:./glog -v=4 -log_dir=log -vmodule=main=5,日志中将会打印出级别为5的日志:

Log file created at: 2019/09/08 18:11:36
Running on machine: maqianos
Binary: Built with gc go1.10.4 for linux/amd64
Log line format: [IWEF]mmdd hh:mm:ss.uuuuuu threadid file:line] msg
I0908 18:11:36.096301     557 main.go:12] Level 3 log
I0908 18:11:36.100119     557 main.go:13] Level 5 log

三、自定义修改

3.1 修改日志WARN为DEBUG

默认是没有DEBUG日志的,WARN日志用得少可以把WARN日志改成DEBUG。在glog.go源码文件中,修改以下内容:

const severityChar = "IWEF" // 改为"IDEF"

var severityName = []string{
    infoLog:  "INFO",
    warningLog: "WARN" // 改为"DEBUG",
    errorLog: "ERROR",
    fatalLog: "FATAL",
}

这里实现简单,改起来也简单。

3.2 默认打印到标准错误

// 搜索函数init
func init() {
    // 注释掉下面两行
    //flag.BoolVar(&logging.toStderr, "logtostderr", false, "log to standard error instead of files")
    //flag.BoolVar(&logging.alsoToStderr, "alsologtostderr", false, "log to standard error as well as files")
    
    // 添加下面两行
    logging.alsoToStderr = false
    logging.toStderr = true

    // Default stderrThreshold is ERROR.
    logging.stderrThreshold = errorLog

    logging.setVState(0, nil, false)
    go logging.flushDaemon()
}

一、strcpy

strcpy是字符串拷贝函数,将一个字符串拷贝到另一个字符串。

代码:

char *strcpy(char *dst, const char *src) { // [1]
    char *p = dst; // [2]

    if (src == NULL || dst == NULL) { // [3]
        return NULL;
    }

    while ((*dst++ = *src++) != '\0'); // [4]

    return p; // [5]
}

实现该函数的注意点:

  1. 传入参数src使用const修饰,避免函数内部修改数据。
  2. 使用p备份dst指针,在拷贝完成之后返回该值。
  3. 需要判断传入参数的合法性,避免出现不合理输入。
  4. 注意判断拷贝完成的标准是src[i] == '\0','\0'也要拷贝到dst字符串中。
  5. 返回拷贝完成的值,更好的支持链式表达式。

高级:

上面的实现是没有区分内存重叠的场景,假设源目地址存在重叠,使用上面的函数实现是有问题的。如图所示:

image.png

dst和src都是指向同一个数组,当把src的第三个元素赋值到dst后,src原有的'\0'被覆盖了,后续继续拷贝就出现问题。

解决这个问题其实很简单,只要从后往前复制src即可解决这个问题。可参考下面memcpy的实现。

二、memcpy

memcpy的作用是把一块内存区域拷贝到另外一个内存地址上面去。

实现:

void *memcpy(void *dst, const void *src, unsigned int count) { // [1]
    char *pdst, *psrc;

    if (dst == NULL || src == NULL || count == 0) { // [2]
        return NULL;
    }

    if (dst == src) { // [3]
        return dst;
    }

    if (dst > src) { // [4]
        pdst = (char *)dst + count - 1;
        psrc = (char *)src + count - 1;

        while (count--) {
            *pdst-- = *psrc--;
        }
    } else { // [5]
        pdst = (char *)dst;
        psrc = (char *)src;

        while (count--) {
            *pdst++ = *psrc++;
        }
    }

    return dst; // [6]
}

需要注意的点:

  1. src使用const修饰,count设置成无符号类型。
  2. 判断传入参数的有效性,避免无效输入。
  3. 判断源目地址相等的情况。
  4. 目的地址大于源地址,从高位向地位复制,避免出现内存重叠的问题。
  5. 目的地址小于源地址,从低位向高位复制。
  6. 返回复制好的目的地址指针以支持链式表达式。

三、memmove

memmove的作用是把一块内存空间的内容移动到另一个内存空间,实现方法和上面的memcpy一致。

其他参考:tcpdump的基本用法

1. 抓取指定网卡上的数据

tcpdump默认抓系统第一块网卡,-i参数可以指定网卡,any表示抓所有网卡:

tcpdump -i eth0 # 抓取eth0上的数据
tcpdump -i any # 抓取所有网卡上的数据

2. 抓取指定IP的数据

抓取来自eth0网卡上IP为192.168.10.1的数据:

tcpdump -i eth0 host 192.168.10.1

3. 抓取指定端口的数据

抓取HTTPS(443端口)的数据:

tcpdump -i eth0 tcp port 443

4. 抓取一个网段的数据

抓取192.168.10.0/24网段的数据:

tcpdump -i eth0 net 192.168.10.0/24

5. 多条件组合

tcpdump可以使用and/or/not来进行多条件组合抓包,当开启组合抓包时,建议使用双引号将语句包起来(否则当条件语句中存在括号等字符时会报错):

tcpdump -i eth0 "host 192.168.10.1 and (tcp port 443 or tcp port 80)" 

抓取来自192.168.10.1的443端口或80端口的数据。

6. 其他选项

  • -n:打印IP地址而不是主机名
  • -v:显示抓到的包的数量
  • -w file:将抓到的包写入文件
  • -c n:只抓取n个包,抓满后程序自动退出