/ Tech

科学有效的上网方法

看这这个挺高大上的标题,好像就像是标题党一样,其实真正的标题用正常人的话来说,就是如何科学的上网,只不过生活在大陆这个环境下,已经习惯了用各种隐晦的词来描述某些东西。算了,标题党就标题党吧。

写这篇的主要原因也是因为最近腾讯云把我的一个香港服务器给封禁了,理由也是意料之中的「存在通过技术手段使其成为跨境访问节点等行为」,刚开始还以为是先发出警告,等我连接服务器的时候发现直接是把服务器给封掉了,连22端口都没得用。幸亏部署的服务都是静态方式存储的,而且大多数还都有备份,这要是没有备份的话,真的要气死人了。

没办法只好重新搞一台服务器了,不过这次选用的国外的服务器,服务器拿到手就直接通过脚本部署了一套SSR,刚用了几个小时就被Q了,切换IP同样操作之后还是没过多久就又不行了,于是尝试更换方案,经过了解尝试使用V2Ray+websocket+tls+web的方式来构建服务。

V2Ray+websocket+tls+web是目前最稳定的翻Q技术之一,即使在敏感时期也稳如泰山。和SSR的流量混淆不同,V2Ray+ws+tls用真正的https流量FQ,没有必要做任何混淆。在长城看来,流量和不计其数的https流量没有任何区别。但是如果GFW尝试主动嗅探的话,发现流量的目的地没有真正的网站,从而被识破然后被Q。所以要在这个服务外面加一个真正的网站做掩护,从而假装是访问真正的网站。

Cloudflare配置DNS

这次部署过程中,使用的是Cloudflare作为DNS服务器,Cloudflare是世界上最大的CDN提供商,全球半数的网站都在使用Cloudflare。使用不用担心隐私泄露或钓鱼风险,而且也可以提供免费的CDN服务,只不过对于国内来说,访问速度可能会慢一点。

jFu3MfCB

  • DNS类型为A类型
  • 名称为要解析的域名地址,可以顶级,也可以是二级域名
  • 内容就是要解析的IP地址
  • 代理状态为「仅限DNS」,注意小云朵是灰色

配置完成之后可以使用 ping命令测试是否可以ping通,如果可以,那就说明配置没问题。

V2Ray安装和配置

网上已经有很多一键安装脚本,这里选用的是 https://github.com/v2fly/fhs-install-v2ray,此脚本需要在root用户下运行,并且会安装如下文件:

installed: /usr/local/bin/v2ray
installed: /usr/local/bin/v2ctl
installed: /usr/local/share/v2ray/geoip.dat
installed: /usr/local/share/v2ray/geosite.dat
installed: /usr/local/etc/v2ray/config.json
installed: /var/log/v2ray/
installed: /var/log/v2ray/access.log
installed: /var/log/v2ray/error.log
installed: /etc/systemd/system/v2ray.service
installed: /etc/systemd/system/v2ray@.service

此脚本会配置自动运行脚本。自动运行脚本会在系统重启之后,自动运行 V2Ray。目前自动运行脚本只支持带有 Systemd 的系统,以及 Debian / Ubuntu 全系列。

其中路径/usr/local/etc/v2ray/config.json就是配置文件的路径,安装完成之后只用修改这个文件即可。

关于V2Ray的配置网上已经有很多,我们只需要下载一个,其中需要修改的地方改成我们自己的即可。可以参考我的配置:

{
    "inbound": {
        "protocol": "vmess",
        "listen": "127.0.0.1",
        "port": 8080,
        "settings": {
            "clients": [
                {
                    "id": "◆◆◆◆◆◆◆◆◆◆◆◆",
                    "afterId": 0,
                }
            ]
        },
        "streamSettings": {
            "network": "ws",
            "wsSettings": {
                "path": "/★★★★★★★★★★★★"
            }
        }
    },
    "outbound": {
        "protocol": "freedom",
    "ip": ["geoip:provate"],
    "outboundTag": "blocked"
    }
}

在这个配置文件中,需要将标记的信息换成自己的即可:

"◆◆◆◆◆◆◆◆◆◆◆◆":uuid,自己生成或者通过在线网站生成,例如:63c0042a-4a85-4d03-a488-3ba383142461

"★★★★★★★★★★★★":这个地方填写的则是一个随机字符串,越没有规律越好,越乱越好,可以在键盘上胡乱打一串或者程序生成。其实这个字符串就是websocket路径,在配置客户端的时候需要用到。

Nginx配置反代

因为上面已经配置了DNS的解析服务,而且也不适合直接用IP,所以这时候就需要用Nginx来代理websocket,配置文件内容如下:

server {
    ### 1:
    server_name ●●●●●●●●●●●●;

    listen 80 reuseport fastopen=10;
    rewrite ^(.*) https://$server_name$1 permanent;
    if ($request_method  !~ ^(POST|GET)$) { return  501; }
    autoindex off;
    server_tokens off;
}

server {
    ### 2:
    ssl_certificate /etc/letsencrypt/live/●●●●●●●●●●●●/fullchain.pem;

    ### 3:
    ssl_certificate_key /etc/letsencrypt/live/●●●●●●●●●●●●/privkey.pem;

    ### 4:
    location /★★★★★★★★★★★★
    {
        proxy_pass http://127.0.0.1:8964;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_requests 10000;
        keepalive_timeout 2h;
        proxy_buffering off;
    }

    listen 443 ssl reuseport fastopen=10;
    server_name $server_name;
    charset utf-8;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_requests 10000;
    keepalive_timeout 2h;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_ecdh_curve secp384r1;
    ssl_prefer_server_ciphers off;

    ssl_session_cache shared:SSL:60m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 10s;

    if ($request_method  !~ ^(POST|GET)$) { return 501; }
    add_header X-Frame-Options DENY;
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options nosniff;
    add_header Strict-Transport-Security max-age=31536000 always;
    autoindex off;
    server_tokens off;

    index index.html index.htm  index.php;
    location ~ .*\.(js|jpg|JPG|jpeg|JPEG|css|bmp|gif|GIF|png)$ { access_log off; }
    location / { index index.html; }
}

看上去很长,实际上只有四处需要填写,配置文件里用#1,#2,#3,#4标出了位置,把标符号的地方换成你自己的信息。

●●●●●●●●●●●●:标注“●”的地方填写域名,注意这里的域名带www

★★★★★★★★★★★★:标注“★”的地方填写一个随机字符串,这个随机字符串必须和V2Ray配置中的一样,不然无法工作。注意不要删掉前面的斜杠。

最后,把Nginx的配置文件另存为default.conf(注意扩展名就是.conf),放在/etc/nginx/conf.d/下。

配置SSL证书

为了用真正的https流量翻墙,网站就必须有合法的SSL证书。可以用自动化工具Certbot申请证书,只要把以下命令复制到命令窗口,依次执行即可。

这里说的“证书”,实际指的是“数字证书”。当然申请完全是免费的,申请时需要邮箱地址。如有必要,可以使用匿名邮箱。

安装certbot

由于服务器系统自带的Python环境是v3.7,但是没有安装pip,所以首先安装pip工具。

apt install python3-pip

安装好pip之后即可安装certbot

pip3 install certbot

生成证书

certbot certonly --standalone --agree-tos -n -d www.●●●●●●●●●●●● -d ●●●●●●●●●●●● -m ●●●●●●●●●●●●@●●●●●●●●●●●●

第一个-d加一个带www的域名,第二个-d加一个不带www的域名,-m后面加你的电子邮箱。注意前后要带空格。

运行这条命令后,如果显示:

IMPORTANT NOTES:

- Congratulations! Your certificate and chain have been saved at:
  /etc/letsencrypt/live/www.hrw1rdzqa7c5a8u3ibkn.website/fullchain.pem
  Your key file has been saved at:
  /etc/letsencrypt/live/www.hrw1rdzqa7c5a8u3ibkn.website/privkey.pem
  Your cert will expire on 2020-06-04. To obtain a new or tweaked
  version of this certificate in the future, simply run certbot
  again. To non-interactively renew *all* of your certificates, run
  "certbot renew"

- Your account credentials have been saved in your Certbot
  configuration directory at /etc/letsencrypt. You should make a
  secure backup of this folder now. This configuration directory will
  also contain certificates and private keys obtained by Certbot so
  making regular backups of this folder is ideal.

- If you like Certbot, please consider supporting our work by:

  Donating to ISRG / Let's Encrypt:  https://letsencrypt.org/donate
  Donating to EFF:                    https://eff.org

表示证书生成成功。

注意:在生成证书的时候容易出现问题,就比如我在生成证书的时候就出现如下错误:

x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -Wdate-time -D_FORTIFY_SOURCE=2 -g -fdebug-prefix-map=/build/python2.7-PPrPZj/python2.7-2.7.15=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/ffi -I/usr/include/libffi -I/usr/include/python2.7 -c c/_cffi_backend.c -o build/temp.linux-x86_64-2.7/c/_cffi_backend.o
  c/_cffi_backend.c:15:10: fatal error: ffi.h: No such file or directory
   #include <ffi.h>
            ^~~~~~~
  compilation terminated.
  error: command 'x86_64-linux-gnu-gcc' failed with exit status 1

解决办法是安装相关依赖即可。

apt install libffi-dev

同样的因为这个问题导致的cryptographypyopenssl版本问题,也可以通过升级安装包解决。

另外通过这种方式生成的证书时限只有三个月,所以我们通过定时任务,让他自从申请证书,从而达到证书的 自动续命

echo "0 0 1 */2 * service nginx stop; certbot renew; service nginx start;" | crontab

启动服务

测试V2Ray配置是否正常

/usr/local/bin/v2ray -test -config=/usr/local/etc/v2ray/config.json

如果显示:

V2Ray 4.x.x (V2Fly, a community-driven edition of V2Ray.)
A unified platform for anti-censorship.
Configuration OK.

测试nginx配置是否正常

nginx -t

如果显示:

nginx: the configuration file /etc/nginx/ngin短网址nf syntax is ok
nginx: configuration file /etc/nginx/ngin短网址nf test is successful

说明配置没有问题。

测试V2Ray和Nginx配置正常之后即可启动服务了。启动服务通过service命令启动

启动V2Ray:

service v2ray start

启动Nginx

service nginx start

配置完成后,可以在浏览器里输入网址,如果显示Nginx的欢迎页面,就说明网址配置成功了! 接下来要做的是上传一个网页模板,这样别人访问你的服务器就会看到一个真的网站。

在网上找一些静态网页模版,放在目录/usr/share/nginx/html/下即可,最好是找一些全英文的网站,存在中文的话可能会增加一些被墙的概率。

客户端配置

这里使用的客户端是著名的代理软件Shadowsockets(小火箭),这是一款iOS生态下全平台通用的一款软件,配置也相对简单。

XUEexvP4

点【服务器】按钮,选择【添加VMess】服务器。

  • 地址:域名地址,当然也可以IP地址
  • 端口:443
  • UUID:就是上面说的V2Ray配置文件里的UUID
  • 额外ID:0,最新的V2Ray版本已经强制为0,至于原因可以自行Google
  • 算法:随便选。
  • 传输方式:选WebSocket;Host:域名地址;路径:即前面的随机字符串,注意前面必须要加上斜杠“/”

image-20220720124353424

  • 别名:随便填。
  • TLS:打开

到这里基本上就算完成了,当然还有一些其他优化项,比如:

  • CDN隐藏域名IP
  • BBR加速(debian10以上自动开启)
  • 配置防火墙

想要了解的可以自行Google,我太懒了,也没有做这些配置,就不一一赘述了,逃。

参考链接:https://pincong.rocks/article/15493

迷之操作|Django版静态Note生成器

Powered by Django

正如这句话而言,目前这个网站是用Django来搭建的,至于当初为什么要选择Django,可能是因为它作为最流行的Web框架之一,开发迅速,以及自带的admin管理,可以迅速成型一个小型站点,YYDS。

Why change

最初这个网站是部署在HK的一个VPS上的,配置也是最低配置,解析是套了一层CF,所以造成国内访问非常慢。不过也可以忍受,就没在这上面折腾。按理说这种网站是用来写的,但我好像不务正业的折腾起来了页面,每换一种风格,用不了多久就觉得看着不舒服,于是又重新改了一套页面,于是又重复于此。

前段时间正好赶上各大云厂商双11做活动,于是在结束的最后一天在良心云上薅了一台国内的VPS,配置要比之前的好一点,而且在国内速度跟之前比也是起飞一般的速度。

在迁移项目的时候突然想到,现在的数据都是在当前主机上的,万一哪一天服务器忘记续费的话,等到期的时候数据岂不是就全丢了,要是把数据全上传到Git,这样不就不用担心了,而且切换服务器的时候,直接把项目拉下来直接就能跑,就像Page服务一样,那岂不是妙哉。

Ready to go

切换数据库从MySQL到SqlLite

因为SqlLite数据库是一个本地文件数据库,对于这种小型站点来说,十分便携方便,而且还可以直接上传到Git,所以就把数据全导入到这个数据库里边了。

全站静态化

首先统计网站的页面都有哪些,每个页面的路由都是什么,通过这些可以组织静态网页存放的目录结构,而且这些在之后的Nginx配置中也十分重要。

比如我的文章路由为https://itsso.cool/blog/django-staticize.html,那么文章 django-staticize.html就应该放在静态文件根目录的 blog目录下。

由于平时写笔记喜欢在Typora上写完之后,在把文章源码复制到在线的编辑器内,再保存上传发布,同时笔记文件也会同时上传到Github以做备份。但是这样就会存在一个问题,假入想要修改其中某一篇内容的话,重复上面的步骤,实在繁琐,要是把笔记文件连同静态文件在项目部署的时候一起打包发布就好了。

所以就开始改造模型,修改文章内容字段为FileField,将笔记文件的相对路径保存到本地数据库内,每次写完就直接把文件上传。

在构建静态文件的时候,数据来源从本地保存的文件来读取,这样一来既可以将笔记备份,同时在修改内容的时候也可以极大的缩减上传步骤。

给Typora配置专属图片上传服务器

在写笔记的时候难免会有一些图片,在上传这些图片的时候就会比较麻烦,因为Typora里边文件存放的只是一个当前主机的绝对路径,直接将笔记发布到线上的时候,图片会因为路径的问题加载不出来。此时就需要将图片保存到一个公网可以访问的一个图床内。

好多人的做法是将自己的图片上传到微博、知乎、csdn等一些知名网站上,然后再将这些图片链接插入到自己的笔记内。这样做虽然是很方便快速,但是也会存在一些问题,就比如微博在某一天,突然给自己的链接加上了防盗链,导致了所有的图片在自己以外的网站不能继续使用。

另外的像GitHub/Gitee Page服务,也可以将自己的图片上传到他们的服务器,但是据说会存在不稳定的情况,想了想还是算了。

自己的东西掌握在自己手里才踏实,于是就自己搞了一个专属于自己的图床服务,这次倒是没有选择Django,而且选择了更合适轻量化的Flask。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
import random
import string
from datetime import datetime

from flask import Flask, request
from markupsafe import escape
from werkzeug.utils import secure_filename

from dotenv import load_dotenv

env_path = '.env'
load_dotenv(dotenv_path=env_path)

STATIC_FOLDER = "images"
STATIC_URL = "/img/"

app = Flask(__name__, static_folder=STATIC_FOLDER, static_url_path=STATIC_URL)

app.config['UPLOAD_FOLDER'] = STATIC_FOLDER
basedir = os.path.abspath(os.path.dirname(__file__))
ALLOWED_EXTENSIONS = {'txt', 'png', 'jpg', 'xls', 'JPG', 'PNG', 'xlsx', 'gif', 'GIF'}

HOSTNAME = os.getenv('HOSTNAME')


# 用于判断文件后缀
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS


# 上传文件
@app.route('/api/upload', methods=['POST'], strict_slashes=False)
def api_upload():
    access_token = request.form.get("access_token") or " "
    if access_token != os.getenv("ACCESS_TOKEN"):
        return "error", 403

    time_path = datetime.now().strftime("%Y/%m/")
    file_dir = os.path.join(basedir, app.config['UPLOAD_FOLDER'], time_path)
    if not os.path.exists(file_dir):
        os.makedirs(file_dir)
    f = request.files['file']  # 从表单的file字段获取文件,myfile为该表单的name值
    if f and allowed_file(f.filename):  # 判断是否是允许上传的文件类型
        filename = secure_filename(f.filename)
        ext = filename.rsplit('.', 1)[-1]  # 获取文件后缀
        ran_str = ''.join(random.sample(string.ascii_letters + string.digits, 8))
        new_filename = ran_str + '.' + ext  # 修改了上传的文件名
        f.save(os.path.join(file_dir, new_filename))  # 保存文件到upload目录
        return HOSTNAME + STATIC_URL + time_path + escape(new_filename)
    else:
        return "不支持的文件格式!"


if __name__ == '__main__':
    app.run(debug=True, port=8080)

仅需这些代码就可以搭建一个轻量的图片上传服务器,Flask真香~

接下来就是配置Typora,在图片复制进来的时候自动上传到服务器

在Typora→Preferences→Image→Image Uploader选择Custom Command,同时在When Insert选择Upload image

kwhpUo4f

部署到服务器,接下来我们选择使用脚本来上传,那么就需要一个脚本来对接我们的图片服务器

#!/bin/bash

# 各类配置信息
base_url="https://img.itsso.cool/api/upload/"
access_token="xxxxxxxxxx"

# 上传图片
for i in "$@"; do
    curl -POST $base_url -H "Content-Type:multipart/form-data" -F "file=@$i" -F "access_token=$access_token"
    echo ""
done

点击Test Uploader就可以测试一下我们脚本是否可以正常上传

swQeFOjS

现在来看一下效果如何,复制一张图片到Typora。

LAbunMRk

配置WebHook让远程服务器自动拉取更新代码

所谓webhook,就是用户可以自定义一种回调函数,通过这种回调函数来改变web应用的行为,这些回调函数可以是web应用的开发者,也可以是第三方用户,并且与原始的web应用没有关联。

这里采用的是Flask搭建一个webhook服务,并通过该服务触发脚本来完成整个流程操作

首先在GitHub上开通webhook,并添加secret,选择settings→webhooks

使用Flask搭建一个简易的webhook服务,其中要注意对请求来源做验证,GitHub官方已经提供了验证方法

Note: For backward-compatibility, we also include the X-Hub-Signature header that is generated using the SHA-1 hash function. If possible, we recommend that you use the X-Hub-Signature-256 header for improved security. The example below demonstrates using the X-Hub-Signature-256 header.

接下来就是完整的代码:webhook.py

import hmac
from flask import Flask, request, jsonify
import subprocess

app = Flask(__name__)
# github中webhooks的secret
github_secret = 'xxxxxxxx'

def encryption(data):
    key = github_secret.encode('utf-8')
    obj = hmac.new(key, msg=data, digestmod='sha256')
    return obj.hexdigest()

@app.route('/hook', methods=['POST'])
def post_data():
    """
    github加密是将post提交的data和WebHooks的secret通过hmac的sha256加密,放到HTTP headers的
    X-Hub-Signature256参数中
    """
    post_data = request.data
    token = encryption(post_data)
    # 认证签名是否有效
    signature = request.headers.get('X-Hub-Signature-256', '').split('=')[-1]
    if signature != token:
        return "token认证无效", 401
    # 运行shell脚本,更新代码
    subprocess.run(["bash", "auto_deploy.sh"])
    return jsonify({"status": 200})

if __name__ == '__main__':
    app.run(port=9000)

要触发的脚本文件:auto_deploy.sh

cd "$(dirname "$0")"
echo '--------Git fetch------------'
git fetch
echo '--------Git merge------------'
git merge
echo '-----Already up-to-date------'
echo '----- reload nginx-----'
nginx -s reload

接下来将webhook服务部署好,在项目文件夹下push代码的时候,就会触发该hook,在远程服务器自动将更新的代码拉取下来。

到此为止,用了奇奇怪怪的方法实现了自己奇奇怪怪的想法。

其实现在已经有很多很成熟的静态博客生成器,比如Hugo,Jekyll以及Hexo等等,以前搞过一次,后来网站没了,再加上觉得上传方式并不是那么Geek,也有可能是我没找到一些方便的方法,就直接放弃了。

后来重新搭建网站的时候,就打算干脆直接搭建一个动态的网站,至少很多东西要比静态的灵活方便。

至于现在为什么又变成这样了,那可能是

脑子抽风了吧。

Django批量创建时出现bulk_create内存异常

背景

因为需要往项目数据库上传大量数据,数据是以文件的方式存储,所以采用django的bulk_create批量读取并上传,但是在上传过程中发现,上传程序占用的内存一直在上升,甚至到最后直接把内存占满了。

排查问题

刚开始首先怀疑的就是程序代码有问题导致内存没有释放,自己看没发现什么问题,请教同事帮忙看也没有发现什么问题,所以干脆就硬着头皮去试代码。在每次循环之后都加入gc.collect(),尝试主动释放内存,发现问题仍然存在。

于是开始尝试内存排查工具tracemalloc来排查什么地方一直在增加内存

def batch_insert(filepath):
    tracemalloc.start()
    start_snapshot=tracemalloc.take_snapshot()  # 建立快照
    path = Path(filepath)
    for i, p in enumerate(sorted(path.glob('**/*.json')), start=1):
        with p.open(encoding='utf-8') as f:
            data = json.load(f)
    			... # 内容整理
        with transaction.atomic():
            try:
              	# 批量插入数据库
                Regulation.objects.bulk_create(rules)
            except Exception as e:
                print(e)

        gc.collect()	# 主动释放内存,
       	end_snapshot = tracemalloc.take_snapshot()
        top_stats = snapshot.compare_to(start_snapshot, 'lineno')
        for index, stat in enumerate(top_stats[:50], 1):
            frame = stat.traceback[0]
            print("#%s: %s:%s: %.1f KiB"
                  % (index, frame.filename, frame.lineno, stat.size / 1024))

RFK6GbLQ

ewSJd0CP

发现内存主要的增长是在第一行,而且后面也主要和MySQLdb有关,那么就去看看到底是怎么回事,点开源码在发现django/utils/encoding.py:62

def force_str(s, encoding='utf-8', strings_only=False, errors='strict'):
    """
    Similar to smart_str(), except that lazy instances are resolved to
    strings, rather than kept as lazy objects.

    If strings_only is True, don't convert (some) non-string-like objects.
    """
    # Handle the common case first for performance reasons.
    if issubclass(type(s), str):
        return s
    if strings_only and is_protected_type(s):
        return s
    try:
        if isinstance(s, bytes):
            s = str(s, encoding, errors)
        else:
            s = str(s)
    except UnicodeDecodeError as e:
        raise DjangoUnicodeDecodeError(s, *e.args)
    return s

虽然有注解,但还是不明所以,那最起码看看是谁在调用这个方法总也行吧

grep -nr 'force_str('

发现还不少调用,那就找到关键的信息继续往上追溯

上面那么多虽然都有调用,但是与我们实际的使用情况不符,因为使用的mysql,所以发现红线部分挺符合预期,继续查看代码django/db/backends/mysql/operations.py:171

def last_executed_query(self, cursor, sql, params):
    # With MySQLdb, cursor objects have an (undocumented) "_executed"
    # attribute where the exact query sent to the database is saved.
    # See MySQLdb/cursors.py in the source distribution.
    # MySQLdb returns string, PyMySQL bytes.
    return force_str(getattr(cursor, '_executed', None), errors='replace')

继续查找调用改方法的地方

grep -nr 'last_executed_query('

找到源码django./db/backends/utils.py:113

@contextmanager
def debug_sql(self, sql=None, params=None, use_last_executed_query=False, many=False):
    start = time.monotonic()
  try:
    yield
  finally:
    stop = time.monotonic()
    duration = stop - start
    if use_last_executed_query:
      sql = self.db.ops.last_executed_query(self.cursor, sql, params)
      try:
        times = len(params) if many else ''
        except TypeError:
          # params could be an iterator.
          times = '?'
          self.db.queries_log.append({
            'sql': '%s times: %s' % (times, sql) if many else sql,
            'time': '%.3f' % duration,
          })
          logger.debug(
            '(%.3f) %s; args=%s',
            duration,
            sql,
            params,
            extra={'duration': duration, 'sql': sql, 'params': params},
          )

研究一下代码发现,sql = self.db.ops.last_executed_query(self.cursor, sql, params),在批量上传的时候,会将所有要上传的内容变成一条sql语句,到此为止目前还没发现有什么异常。

但是看到下面这一段,Django将生成的sql语句保存起来,那这个对象会清空之前保存的sql吗?

self.db.queries_log.append({
                'sql': '%s times: %s' % (times, sql) if many else sql,
                'time': '%.3f' % duration,
            })
            logger.debug(
                '(%.3f) %s; args=%s',
                duration,
                sql,
                params,
                extra={'duration': duration, 'sql': sql, 'params': params},
            )

经过Debug发现,是不会的,每生成一条sql就添加到self.db.queries_log里,只要程序没有停止,那么这里边的sql就永远不会消失,所以真相大白,原来就是这个家伙导致的内存一直飙升。

继续向上看,寻找是否存在参数可以选择是否保存这些sql,在django/db/backends/utils.py:97发现这段代码

def execute(self, sql, params=None):
    with self.debug_sql(sql, params, use_last_executed_query=True):
            return super().execute(sql, params)

发现确实存在一个参数use_last_executed_query,但是这个参数已经写死在代码里。。。

尝试修改源码将use_last_executed_query设置为False,运行代码发现问题解决。

后来有尝试将sql语句放入self.db.queries_log这段代码注释掉,运行代码仍然可以解决。

至此已经发现问题的真正根源出在什么地方了,但是通过修改源码的方式总归是不合适的,继续尝试有没有Django自带的方案。经过查找发现django/db/__init.py:26

# Register an event to reset saved queries when a Django request is started.
def reset_queries(**kwargs):
    for conn in connections.all():
        conn.queries_log.clear()

于是尝试在代码中引入该方法

from django.db import reset_queries

...
# 插入数据库之前首先清空
reset_queries()
# 批量插入数据库
Regulation.objects.bulk_create(rules)

发现问题完美的得到解决,完美!

后来在官方文档中发现,已经有这个方法的相关文档

dmTAMrGI

文档中还说只有在DEBUG=True时,这些sql语句才会保存起来以便有需要的时候查看sql。

这也解答了我心中的一个疑问,系统运行时产生的sql为什么要保存起来呢,现在看来是我知识浅薄了。

所以最终这个问题的解决方案有两个:

一、将DEBUG设置为False

二、手动清除,引入django.db.reset_queries

就第一种方案而言,只有在生产环境下,DEBUG选项才会为False,所以在自己的电脑上或者测试机上运行,还是第二种方案比较好。

Elasticsearch相关汇总

在使用Elasticsearch的过程中,难免会要去官网翻看文档,由于目前还没有读完文档,以至于在找一些没用过的API时还挺费劲,有时候甚至还可能找不到。因此就把目前已经用到过的地方在这里汇总记录一下,方便以后碰到的话可以直接去查看。

Index

Aliases

索引的一个别名,在某些情况下非常有用,比如在无缝切换索引的时候。

Mappings

索引的mapping定义十分重要,他决定了我们的数据是如何保存在索引内,以及保存的数据都有什么字段,各个字段的数据类型又是什么。

Setting

Query

Full text queries

全文检索相关,主要包含match querymatch_bool_prefix querymatch_phrase querymatch_phrase_prefix querymulti_match queryquery_string querymatch_bool_prefix querymatch_phrase querymatch_phrase_prefix querymulti_match queryquery_string query

Compound queries

混合索引,包含bool queryboosting queryconstant_score querydis_max queryfunction_score query

Function score query

用户可以通过自定义一个或多个查询语句来提高某些文档的比分权重,

还可以通过script_score使用脚本给每个文档重新打分

Highlight

Prefix query

使用前缀查询可以返回前缀为指定前缀的文档,多用于即时搜索一类的提示。

Match phrase prefix query

当需要对一个短语或词组进行前缀查询时,就需要用到来进行搜索了

Named query

通过使用_name参数可以在多字段查询时知道是哪个子查询语句命中了该文档,并将结果返回在每个响应文档的matched_queries字段内。

Nested query

Exists query

在某些情况下,并不是所有的字段都存在确切的值,可以通过Exists来或者筛选包含某些字段的文档,同时配合must_not可以来筛选所有存在该字段的文档。

Scripts

ES的脚本语言是painless,语法与Java类似,可直接按照Java的语法来编写检索脚本,具体可见地址:Shard API

这里只记录一下自己使用到的,以便以后再遇到可直接CV。

删除数组内满足条件的元素

使用removeIf来完成,例如删除ID为10的元素

{
  "script": {
    "source": "ctx._source.members.removeIf(list_item -> list_item.id == params.member_id)",
    "lang": "painless",
    "params": {
      "member_id": 10
    }
  }
}

判断数组内是否包含某一个对象

使用contains来完成,返回包含name张三的文档

{
  "script": {
    "source": "ctx._source.members.contains(params.name)",
    "lang": "painless",
    "params": {
      "name": "张三"
    }
  }
}

根据时间提高某些文档的权重

使用时间格式化方法toInstanttoEpochMilli来完成,将时间转换成毫秒级权重因子

{
    "script": {
        "lang": "painless", 
        "source": "double dateScore; try {dateScore = Math.abs(doc['enforcementDate'].value.toInstant().toEpochMilli()/1e12);} catch (Exception e) {dateScore=0;} return dateScore;"
    }
}

Run Elasticsearch by docker

1. Download the docker image of Elasticsearch, taking version 7.6.0 as an example

    docker pull elasticsearch:7.6.0

2. Create a container and run it.

if your command is:

   docker run -d --name es -p 9200:9200 -p 9300:9300 elasticsearch:7.6.0

It may exit shortly after starting. To find out the reason, view logs by log command:

   docker logs es

Some error message like max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] may appear. Go the following command:

   # change the variable
   sysctl -w vm.max_map_count=262144
   # check the variable value
   sysctl -n vm.max_map_count

If the error message is the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured, which means the container has wrong configuration and can be corrected by setting those prompted environment variables or setting to standalone development mode by discovery.type=single-node.

In short, the correct command is:

   docker run -d --name es -p 9200:9200 -p 9300:9300 -e discovery.type=single-node elasticsearch:7.6.0

Using IK Chinese segmentation plugin.

1. Download the plugin.

The plugin version must equal the Elasticsearch version. Version 7.6.0 download link is https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.0/elasticsearch-analysis-ik-7.6.0.zip

2. Unzip to an empty directory, which is referred to as *$IK*.

3. Copy the $IK into the directory of the container's plugin.

   docker cp $IK es:/usr/share/elasticsearch/plugins/ik

This plugin provides analyzer and tokenizer named ik_smart and ik_max_word where ik_smart splits by the coarsest granularity, while ik_max_word will exhaust all kinds of split combinations.

Find more info at IK.

Fulltext retrieval with whoosh and Jieba

Environment dependencies

pip install django-haystack
pip install jieba
pip install whoosh

Environment configuration

Add this configuration in settings.py

INSTALLED_APPS = (
    'haystack',	#register fulltext searching framework
    )

#the configuration of fulltext searching
HAYSTACK_CONNECTIONS = {
    'default': {
    	# use the whoosh search engine
    	'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
    	# Specifies the default path of the index files generated by the index data corresponding to the keyword. When using the custom index file, write the custom file path here.
    	'PATH': os.path.join(BASE_DIR,'whoosh_index'), #  the file path of the index files.
    	}
}

# Auto generate indexes when add, change and delete data in database tables.
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

Create search_indexs.py in applications which need support searching.

from haystack import indexes
from apps.blog.models import Article


class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        return Article

    def index_queryset(self, using=None):
        return self.get_model().objects.filter(status='p')

In the templates folder of the project, create folder structure like search/indexes/article/article_text.txt, where article is the lowercased model name.

# Specifies which fields in the table to index
{{ object.title }}	# index Title field
{{ object.body }}	# index Body field

Add search route

path('search', include('haystack.urls'), name='search'),

Add search form in the template.

<form action="/search" method="get">
            {% csrf_token %}
                <span id="searchAria" tabindex="0" onclick="searching()" onblur="offsearch()">
                    <input type="text" id="searchInput" style="display: none; border: none;" name="q">
                    <input type="submit" value="Search" style="border: none; display: none;" id="submitInput">
                    <a href="javascript:void(0);"  class="navPlugs"><i class="fa fa-search" aria-hidden="true"></i></a>
                </span>
</form>

tips: there must be an input tag whose attribute named name equals q in the form.

The following is the page of search results.

    <div class="jupe main-body">
        <ul class="post-list">
            {% if query and page.object_list %}
                {% for result in page.object_list %}
                    <li class="post-item">
                        <a class="post-title"
                           href="{{ result.object.get_absolute_url }}"
                           title="{{ result.object.title }}">{{ result.object.title | truncatesmart:34 }}</a>
                        <span class="post-time">{{ result.object.create_time | date:"Y.m.d" }}</span>
                    </li>
                {% empty %}
                    <p>Not found</p>
                {% endfor %}

                {% if is_paginated %}{% load_pages %}{% endif %}
            {% else %}
                <h3>Found nothing. Try to search by another keyword</h3>
            {% endif %}

        </ul>
    </div>

Build index

python manage.py rebuild_index

Configure Jieba Chinese Search

Because the default engine of whoosh doesn't support Chinese, u need to improve it.

Copy the default engine file \site-packages\haystack\backends\whoosh_backend.py to the project folder and rename it to whoosh_cn_backend.

Open it and import Jieba Chinese analyzer from jieba.analyse import ChineseAnalyzer.

Replace StemmingAnalyzer in the file with ChineseAnalyzer

Change the file path of search engine to custom path in settings.py

'ENGINE': 'apps.search.whoosh_cn_backend.WhooshEngine'

Rebuild index python manage.py rebuild_index

It supports Chinese search now.

OAuth2简述

什么是OAuth2

OAuth2是一个服务开放(授权)标准,它表示允许用户授权第三方应用访问该用户在另外一个服务器存储的用户信息,而不用将存储在该服务器的用户名以及密码或所有用户信息告知给第三方应用。

OAuth2的应用场景

当你想使用QQ登录的第三方应用时,此时第三方应用需要获取你的QQ信息(昵称、头像、openid等),但是又不能直接用户名和密码等信息。那么此时就需要你登录你的QQ服务器,然后授权给该第三方应用一部分信息。而OAuth2就是为了实现上述目标而制定的一种规范。

Oauth2的授权模式

  • 授权码(authorization code)
  • 隐藏式(implicit)
  • 密码式(resource owner password credentials)
  • 客户端凭证(client credentials)

授权码

授权码模式,指的是第三方应用先申请一个授权码,然后再拿着该授权码获取令牌。

img

  1. 用户访问第三方应用客户端,随后该客户端跳转到认证服务器,跳转的过程中附带上重定向URI。
  2. 用户在认证服务器给予授权之后,跳转到事先定义好的redirect_uri,并返回上一个授权码,该授权码是与当前客户端绑定的,无法被其他客户端使用。
  3. 客户端收到该授权码,携带上步骤1中的redirect_uri,向认证服务器申请令牌。请求成功后认证服务器会返回一个令牌,该令牌存在一个有效时间。
  4. 客户端拿到该令牌之后,将该令牌携带在请求中请求资源服务器,资源服务器判断该令牌的有效性,最终判断是否返回有效的资源信息。

隐藏式

当有些网站时纯前端架构的时候,就无法通过后端来使用上述的授权模式了,令牌必须存在前端。该模式下不通过第三方应用的服务器,直接在浏览器里想认证服务器申请令牌,跳过了授权码的步骤。所有步骤都是在浏览器内完成,令牌对访问者是可见的,并且客户端不需要认证。

img

  1. 客户端在页面内放置一个链接,该链接指向认证服务器
  2. 用户点击授权,给该客户端授权
  3. 认证服务器在授权之后将跳转到客户端指定的redirect_uri,并将令牌包含在URI中,其中令牌并不是以查询参数的形式存储在URI中,而是以锚点的形式存储。
  4. 浏览器想服务器发起请求,资源服务器返回一个网页,网页内包含一个可以获取上述令牌的脚本
  5. 浏览器执行脚本,提取令牌,发送给客户端
  6. 客户端拿到令牌向资源服务器请求,资源服务器判断该令牌的有效性,最终判断是否返回有效的资源信息。

密码式

听名字我们就可以知道,该授权模式需要密码来进行。其实质是,如果你高度信任某个应用,那么可以直接告诉这个应用你的用户名和密码,然后该应用拿到这些信息去申请令牌。

img

  1. 用户向第三方应用提供用户名和密码
  2. 该应用将用户名和密码发送给认证服务器,然后请求令牌
  3. 认证服务器确认无误后,想客户端提供令牌
  4. 客户端拿到令牌向资源服务器请求,资源服务器判断该令牌的有效性,最终判断是否返回有效的资源信息。

客户端凭证

客户端凭证模式下,即是通过客户端的名义,而不是用户的名义去获取令牌。该模式下可以在纯后端进行,常见的参数有client_id、client_secret。

img

  1. 客户端向认证服务器发送一个携带client_id和client_secret的请求
  2. 认证服务器接收到客户端请求,并验证client_id和client_secret的有效性,如果有效这向客户端提供令牌。

What is JWT?

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

Application scenarios

Single Sign On

This is the most common scenario to use JWT. In a multi-system service architecture, after a user logged in to one system at first time, there is no need to log in repeatedly when using other systems.

Authorization

After a user logged in, every subsequent request contains JWT. When the JWT is verified, the user is allowed to access the resource he should access.

Components of JWT

JWT consists of three parts: Header, Payload, Signature, and they are connected with a dot.

  • The Header consists of two parts: the type of token and the signing algorithm being used, e.g. :
{
    'alg': "HS256",
    'typ': "JWT"
}
  • The Payload is actually a place to store valid information which has three types of claims.

Registered claims: Predefined claims which are not mandatory but recommended. Such as iss(issuer) and exp(expiration time)

Public claims: They can be defined at will.

Private claims: These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.

  • The Signature is a digital signature to prevent data from being tampered.

How is JWT generated

JWT is divided into three parts. How do these three parts come together to form a completed JWT?

  • The Header is encoded by base64 to get the first part.
  • The Payload is encoded by base64 to get the second part.
  • Through the algorithm in Header with a secret sign that encoded Header and encoded Payload and Connected with dot.

Tips: Jwt is not encrypted, so it's better not to store some sensitive information directly in JWT.

Workflow of JWT

After successful login for the first time, the user will get a JWT. This JWT is the certificate for the user to interact with the server, that is, the server uses this token to determine which user the requester is. When the user wants to access the resources on the server, the token was sent, which is usually placed in authorization, using bearer schema. For example:

Authorization: Bearer token

What are the advantages over traditional session authentication?

Session authentication

Because HTTP is a stateless protocol, we provide our user name and password for authentication when we first access it. Then, in the second visit, we still need to carry out a user authentication. Because according to the HTTP protocol, we don't know who the originator of the request is, in order to identify which user initiated the request, we can only save a copy of the user's information on the server, and then pass the same information to the user, so that the user can save it in cookies. In the next request, you only need to carry the information in the cookies. According to the submitted cookies, the server can determine which user it is. This is the traditional authentication based on session.

Problems in session based authentication

Session: after each user has been authenticated by our application, our application needs to make a record on the server to facilitate the authentication of the next request. Generally speaking, the session is saved in memory. With the increase of authenticated users, the cost of the server will increase significantly.

Scalability: after user authentication, the server makes authentication records. If the authentication records are saved in memory, it means that the next request of the user must be on this server, so that the authorized resources can be obtained. This also means that the expansion ability of the application is limited.

CSRF: because user identification is based on cookie, if cookie is intercepted, users will be vulnerable to cross site request forgery attack.

What are the advantages of JWT based authentication

  • Tokens are stored in the client, completely stateless and scalable.
  • Security: token is different from a cookie. Each request will send a token. Since no cookie is sent, it can also prevent CSRF attacks.

reference from https://jwt.io/introduction

MySQL创建用户和配置远程连接

创建远程连接用户

在某些情况下,可能别人需要连接你的数据库进行操作,出于安全考虑,我们需要新建一个用户,让该用户只具有一部分的操作权限操作数据库。

当然如果只有你一个人使用这个数据库的话,也可以跳过这一步,直接选择root用户来操作。

创建用户

使用help命令查看如何创建用户

help create user
Name: 'CREATE USER'
Description:
Syntax:
CREATE USER user_specification
    [, user_specification] ...

user_specification:
    user
    [
        IDENTIFIED BY [PASSWORD] 'password'
      | IDENTIFIED WITH auth_plugin [AS 'auth_string']
    ]

根据帮助说明,接下来我们来创建用户

create user 'zjc'@'localhost' # 创建一个用户zjc,不需要密码即可登录,但只可以在本机登录
create user 'zjc'@'%'	# 创建一个用户zjc,不需要密码即可登录,可以在任意主机登录
create user 'zjc'@'%' identified by '123456'	# 创建一个用户zjc,登录时需要密码123456才可以登录,会自动将密码加密
create user 'zjc'@'%' identified by password '123456' # 创建一个用户zjc,登录时需要密码123456才可以登录,会将密码明文存储

修改密码

命令:

set password for 'username'@'host' = PASSWORD('newpassword');

查看帮助命令查看详情

> help set password
Name: 'SET PASSWORD'
Description:
Syntax:
SET PASSWORD [FOR user] =
    {
        PASSWORD('cleartext password')
      | OLD_PASSWORD('cleartext password')
      | 'encrypted password'
    }


修改密码:

set password for 'zjc'@'%' = PASSWORD("654321")

删除用户

命令:

drop user username

用户授权

查看用户目前的权限

> show grants for zjc;
+---------------------------------+
| Grants for zjc@%                |
+---------------------------------+
| GRANT USAGE ON *.* TO `zjc`@`%` |
+---------------------------------+

USAGE相当于一个占位符,表示目前zjc用户什么权限都没有,需要我们给他授予权限

授权该用户权限

命令:

grant privileges ON databasename.tablename to 'username'@'host'

同样使用help命令详情

> help grant
Name: 'GRANT'
Description:
Syntax:
GRANT
    priv_type [(column_list)]
      [, priv_type [(column_list)]] ...
    ON [object_type] priv_level
    TO user_specification [, user_specification] ...
    [REQUIRE {NONE | ssl_option [[AND] ssl_option] ...}]
    [WITH with_option ...]

GRANT PROXY ON user_specification
    TO user_specification [, user_specification] ...
    [WITH GRANT OPTION]

object_type:
    TABLE
  | FUNCTION
  | PROCEDURE

priv_level:
    *
  | *.*
  | db_name.*
  | db_name.tbl_name
  | tbl_name
  | db_name.routine_name

user_specification:
    user
    [
        IDENTIFIED BY [PASSWORD] 'password'
      | IDENTIFIED WITH auth_plugin [AS 'auth_string']
    ]

ssl_option:
    SSL
  | X509
  | CIPHER 'cipher'
  | ISSUER 'issuer'
  | SUBJECT 'subject'

with_option:
    GRANT OPTION
  | MAX_QUERIES_PER_HOUR count
  | MAX_UPDATES_PER_HOUR count
  | MAX_CONNECTIONS_PER_HOUR count
  | MAX_USER_CONNECTIONS count

  • privileges:用户的操作权限,如SELECT,INSERT,UPDATE等,如果要授予所的权限则使用ALL
  • databasename:数据库名
  • tablename:表名,如果要授予该用户对所有数据库和表的相应操作权限则可用表示,如.*

给用户zjc授予权限

grant all on *.* to 'zjc'@'%';	

刷新权限

flush privileges;

取消该用户的权限

命令:

revoke privilege ON databasename.tablename FROM 'username'@'host';

help查看详情

> help revoke
Name: 'REVOKE'
Description:
Syntax:
REVOKE
    priv_type [(column_list)]
      [, priv_type [(column_list)]] ...
    ON [object_type] priv_level
    FROM user [, user] ...

REVOKE ALL PRIVILEGES, GRANT OPTION
    FROM user [, user] ...

REVOKE PROXY ON user
    FROM user [, user] ...

说明:同上面的授权部分

配置远程链接

找到配置文件中的[mysqld]一项

# this is only for the mysqld standalone daemon
[mysqld]

#
# * Basic Settings
#
user                    = mysql
pid-file                = /run/mysqld/mysqld.pid
socket                  = /run/mysqld/mysqld.sock
#port                   = 3306
basedir                 = /usr
datadir                 = /var/lib/mysql
tmpdir                  = /tmp
lc-messages-dir         = /usr/share/mysql
#skip-external-locking
skip-name-resolve

# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
# bind-address            = 127.0.0.1

bind-address 注释掉,,然后重启数据库即可

systemctl restart mysql

问题记录

我在尝试用 navicat连接的时候,出现(2013,XXXXXXXX)的错误,这样是由于mysql在接收到客户端链接的时候需要对dns进行反向解析,由于我实在局域网内,所有反向解析是不可能成功的。

解决办法有两种:

  1. 把client的ip写在mysql服务器的/etc/hosts文件里,随便给个名字就可以了。
  2. 在 my.cnf 中加入 skip-name-resolve

于第一种方法比较笨,也不实用,那么 skip-name-resolve 选项可以禁用dns解析,但是,这样不能在mysql的授权表中使用主机名了,只能使用IP。

什么是mysql的dns反解析

mysql接收到连接请求后,获得的是客户端的ip,为了更好的匹配mysql.user里的权限记录(某些是用hostname定义的)。

如果mysql服务器设置了dns服务器,并且客户端ip在dns上并没有相应的hostname,那么这个过程很慢,导致连接等待。

官方解释

How MySQL uses DNS When a new thread connects to mysqld, mysqld will spawn a new thread to handle the request. This thread will first check if the hostname is in the hostname cache. If not the thread will call gethostbyaddr_r() and gethostbyname_r() to resolve the hostname. If the operating system doesn’t support the above thread-safe calls, the thread will lock a mutex and call gethostbyaddr() and gethostbyname() instead. Note that in this case no other thread can resolve other hostnames that is not in the hostname cache until the first thread is ready. You can disable DNS host lookup by starting mysqld with –skip-name-resolve. In this case you can however only use IP names in the MySQL privilege tables. If you have a very slow DNS and many hosts, you can get more performance by either disabling DNS lookop with –skip-name-resolve or by increasing the HOST_CACHE_SIZE define (default: 128) and recompile mysqld. You can disable the hostname cache with –skip-host-cache. You can clear the hostname cache with FLUSH HOSTS or mysqladmin flush-hosts. If you don’t want to allow connections over TCP/IP, you can do this by starting mysqld with –skip-networking.

根据文档说明,如果你的mysql主机查询DNS很慢或是有很多客户端主机时会导致连接很慢,由于我们的开发机器是不能够连接外网的,所以DNS解析是不可能完成的,从而也就明白了为什么连接那么慢了。同时,请注意在增加该配置参数后,mysql的授权表中的host字段就不能够使用域名而只能够使用 ip地址了,因为这是禁止了域名解析的结果。

Linux中的软链接和硬链接

Linux文件系统

在 UNIX 系统中,操作系统为磁盘上的文本与图像、鼠标与键盘等输入设备及网络交互等 I/O 操作设计了一组通用 API,使他们被处理时均可统一使用字节流方式。

换言之,UNIX 系统中除进程之外的一切皆是文件,而 Linux 保持了这一特性。为了便于文件的管理,Linux 还引入了目录(有时亦被称为文件夹)这一概念。目录使文件可被分类管理,且目录的引入使 Linux 的文件系统形成一个层级结构的目录树。

Linux 系统的顶层目录结构

/              根目录
├── bin     存放用户二进制文件
├── boot    存放内核引导配置文件
├── dev     存放设备文件
├── etc     存放系统配置文件
├── home    用户主目录
├── lib     动态共享库
├── lost+found  文件系统恢复时的恢复文件
├── media   可卸载存储介质挂载点
├── mnt     文件系统临时挂载点
├── opt     附加的应用程序包
├── proc    系统内存的映射目录,提供内核与进程信息
├── root    root 用户主目录
├── sbin    存放系统二进制文件
├── srv     存放服务相关数据
├── sys     sys 虚拟文件系统挂载点
├── tmp     存放临时文件
├── usr     存放用户应用程序
└── var     存放邮件、系统日志等变化文件

Linux 与其他类 UNIX 系统一样并不区分文件与目录:目录是记录了其他文件名的文件。使用命令 mkdir 创建目录时,若期望创建的目录的名称与现有的文件名(或目录名)重复,则会创建失败。

硬链接与软链接的联系与区别

我们知道文件都有文件名与数据,这在 Linux 上被分成两个部分:用户数据 (user data) 与元数据 (metadata)。用户数据,即文件数据块 (data block),数据块是记录文件真实内容的地方;而元数据则是文件的附加属性,如文件大小、创建时间、所有者等信息。在 Linux 中,元数据中的 inode 号(inode 是文件元数据的一部分但其并不包含文件名,inode 号即索引节点号)才是文件的唯一标识而非文件名。文件名仅是为了方便人们的记忆和使用,系统或程序通过 inode 号寻找正确的文件数据块。

在 Linux 系统中查看 inode 号可使用命令 stat 或 ls -i(若是 AIX 系统,则使用命令 istat)。

 # stat /home/harris/source/glibc-2.16.0.tar.xz
 File: `/home/harris/source/glibc-2.16.0.tar.xz'
 Size: 9990512      Blocks: 19520      IO Block: 4096   regular file
Device: 807h/2055d      Inode: 2485677     Links: 1
Access: (0600/-rw-------)  Uid: ( 1000/  harris)   Gid: ( 1000/  harris)
...
...
# ls -i -F /home/harris/Desktop/glibc.tar.xz
2485677 /home/harris/Desktop/glibc.tar.xz

为解决文件的共享使用,Linux 系统引入了两种链接:硬链接 (hard link) 与软链接(又称符号链接,即 soft link 或 symbolic link)。链接为 Linux 系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若一个 inode 号对应多个文件名,则称这些文件为硬链接。换言之,硬链接就是同一个文件使用了多个别名(见 图 2.hard link 就是 file 的一个别名,他们有共同的 inode)。硬链接可由命令 link 或 ln 创建。

创建硬链接

link oldfile newfile
ln oldfile newfile

查找有相同 inode 号的文件

df -i --print-type

由于硬链接是有着相同 inode 号仅文件名不同的文件,因此硬链接存在以下几点特性

  • 文件有相同的 inode 及 data block;
  • 只能对已存在的文件进行创建;
  • 不能交叉文件系统进行硬链接的创建;
  • 不能对目录进行创建,只可对文件创建;
  • 删除一个硬链接文件并不影响其他有相同 inode 号的文件。

硬链接不能对目录创建是受限于文件系统的设计。现 Linux 文件系统中的目录均隐藏了两个个特殊的目录:当前目录(.)与父目录(..)。查看这两个特殊目录的 inode 号可知其实这两目录就是两个硬链接。若系统允许对目录创建硬链接,则会产生目录环。

软链接与硬链接不同,若文件用户数据块中存放的内容是另一文件的路径名的指向,则该文件就是软连接。软链接就是一个普通文件,只是数据块内容有点特殊。软链接有着自己的 inode 号以及用户数据块。因此软链接的创建与使用没有类似硬链接的诸多限制:

  • 软链接有自己的文件属性及权限等;
  • 可对不存在的文件或目录创建软链接;
  • 软链接可交叉文件系统;
  • 软链接可对文件或目录创建;
  • 创建软链接时,链接计数 i_nlink 不会增加;
  • 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。

创建软连接

ln -s 源文件路径 目标路径

我们尝试对不存在的文件创建软连接

# ls -li
 total 0

 // 可对不存在的文件创建软链接
 # ln -s old.file soft.link
 # ls -liF
 total 0
 789467 lrwxrwxrwx 1 root root 8 Sep  1 18:00 soft.link -> old.file 

 // 由于被指向的文件不存在,此时的软链接 soft.link 就是死链接
 # cat soft.link 
 cat: soft.link: No such file or directory 

 // 创建被指向的文件 old.file,soft.link 恢复成正常的软链接
 # echo "This is an original file_A" >> old.file
 # cat soft.link
 This is an original file_A

 // 对不存在的目录创建软链接
 # ln -s old.dir soft.link.dir
 # mkdir -p old.dir/test
 # tree . -F --inodes
 .
├── [ 789497]  old.dir/
│   └── [ 789498]  test/
├── [ 789495]  old.file
├── [ 789495]  soft.link -> old.file
└── [ 789497]  soft.link.dir -> old.dir/

当然软链接的用户数据也可以是另一个软链接的路径,其解析过程是递归的。但需注意:软链接创建时原文件的路径指向使用绝对路径较好。使用相对路径创建的软链接被移动后该软链接文件将成为一个死链接,因为链接数据块中记录的亦是相对路径指向。