2023年3月

谈谈Selenium中浏览器驱动的日志

  • 来源于一位同学,“老师为啥firefox执行后会有日志文件,chrome没有呢?”

比对

  • 你打开chrome浏览器

    from selenium import webdriver
    driver = webdriver.Chrome()
    
  • 这样是没有日志的

  • 同样的代码,你打开firefox

    from selenium import webdriver
    driver = webdriver.Firefox()
    
  • 就会有个日志文件geckodriver.log生成,默认在你上面代码所在目录

  • 内容大致如下

    1678943199197	geckodriver	INFO	Listening on 127.0.0.1:8695
    1678943202290	mozrunner::runner	INFO	Running command: "C:\\Program Files\\Mozilla Firefox\\firefox.exe" "--marionette" "--remote-debugging-port" "8696" "-no-remote" "-profile" "C:\\Users\\SONGQI~1\\AppData\\Local\\Temp\\rust_mozprofileNtWGR9"
    1678943202558	Marionette	INFO	Marionette enabled
    Dynamically enable window occlusion 0
    1678943202562	Marionette	INFO	Listening on port 8705
    WebDriver BiDi listening on ws://127.0.0.1:8696
    1678943202947	RemoteAgent	WARN	TLS certificate errors will be ignored for this session
    console.warn: SearchSettings: "get: No settings file exists, new profile?" (new NotFoundError("Could not open the file at C:\\Users\\SONGQI~1\\AppData\\Local\\Temp\\rust_mozprofileNtWGR9\\search.json.mozlz4", (void 0)))
    DevTools listening on ws://127.0.0.1:8696/devtools/browser/791df03a-8db6-429b-969a-32cd077b3c2f
    
    
  • 的确是这样的,chrome和firefox2个浏览器在selenium中的api都是有差异的

  • 下面的代码可以让你看到2者独有的那些api

    from selenium import webdriver
    driver1 = webdriver.Chrome()
    driver2 = webdriver.Firefox()
    set_of_chrome_api = set([_ for _ in dir(driver1) if _[0]!='_'])
    set_of_firefox_api = set([_ for _ in dir(driver2) if _[0]!='_'])
    print('firefox特有的:',(set_of_chrome_api|set_of_firefox_api)-set_of_chrome_api)
    print('-------------')
    print('chrome特有的:',(set_of_chrome_api|set_of_firefox_api)-set_of_firefox_api)
    

让chrome也有日志

  • 首先来看下Chrome这个类的初始化参数都有哪些

    # selenium\webdriver\chrome\webdriver.py
    class WebDriver(ChromiumDriver):
        def __init__(self, executable_path=DEFAULT_EXECUTABLE_PATH, port=DEFAULT_PORT,
                     options: Options = None, service_args=None,
                     desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
                     chrome_options=None, service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
    
  • 你应该一眼就看到有日志相关的参数了service_log_path

  • 有个默认值DEFAULT_SERVICE_LOG_PATH

  • 往上能看到定义:DEFAULT_SERVICE_LOG_PATH = None

  • 所以chrome就没有日志了

  • 要有就简单了,给一个值就行了,这样

    from selenium import webdriver
    driver = webdriver.Chrome(service_log_path='chrome.log')
    
  • 妥妥的在当前目录下生成了一个chrome.log文件里面也记录着本次操作的日志

  • 多说几句,默认的日志就记录到INFO/WARN这样的级别

  • 如果要加DEBUG的信息,可以这样

    from selenium import webdriver
    driver = webdriver.Chrome(service_args=['--verbose'],
                              service_log_path='chrome.log')
    
    

让firefox没有日志,没那么简单

  • 循着刚才的思路,让firefox没有日志似乎就很简单了

  • 依葫芦画瓢,找到Firefox的定义

    # selenium\webdriver\firefox\webdriver.py
    # 注意,此处的路径跟上面的chrome的并不一样
    class WebDriver(RemoteWebDriver):
        def __init__(self, firefox_profile=None, firefox_binary=None,
                     capabilities=None, proxy=None,
                     executable_path=DEFAULT_EXECUTABLE_PATH, options=None,
                     service_log_path=DEFAULT_SERVICE_LOG_PATH,
                     service_args=None, service=None, desired_capabilities=None,
                     log_path=DEFAULT_LOG_PATH, keep_alive=True):
    
  • 看到了参数service_log_path,一样也有默认值DEFAULT_SERVICE_LOG_PATH

  • 找到定义处,DEFAULT_SERVICE_LOG_PATH = "geckodriver.log"

  • 改为None试试

    from selenium import webdriver
    driver = webdriver.Firefox(service_log_path=None)
    
  • 效果有,但又有点不对

  • 控制台冒出了很多信息

    demo_log.py:2: DeprecationWarning: service_log_path has been deprecated, please pass in a Service object
      driver = webdriver.Firefox(service_log_path=None)
    1678944678241	geckodriver	INFO	Listening on 127.0.0.1:10729
    1678944681310	mozrunner::runner	INFO	Running command: "C:\\Program Files\\Mozilla Firefox\\firefox.exe" "--marionette" "--remote-debugging-port" "10730" "-no-remote" "-profile" "C:\\Users\\SONGQI~1\\AppData\\Local\\Temp\\rust_mozprofileeEN3y9"
    Dynamically enable window occlusion 0
    1678944681583	Marionette	INFO	Marionette enabled
    1678944681587	Marionette	INFO	Listening on port 10743
    WebDriver BiDi listening on ws://127.0.0.1:10730
    1678944681883	RemoteAgent	WARN	TLS certificate errors will be ignored for this session
    console.warn: SearchSettings: "get: No settings file exists, new profile?" (new NotFoundError("Could not open the file at C:\\Users\\SONGQI~1\\AppData\\Local\\Temp\\rust_mozprofileeEN3y9\\search.json.mozlz4", (void 0)))
    DevTools listening on ws://127.0.0.1:10730/devtools/browser/a4f3a501-e642-4540-8998-ae1ea0b3abc5
    
  • 仔细一看,跟之前在geckodriver.log中打印的类似

  • 还多了一句

    DeprecationWarning: service_log_path has been deprecated, please pass in a Service object
    
  • 原来service_log_path被弃用了,需要传递一个Service对象

  • 怎么传递Service对象呢?找到其定义,注意路径

  • 有多个Service在selenium这个库中,你要的是firefox下的

    #  selenium\webdriver\firefox\service.py
    class Service(service.Service):
        """Object that manages the starting and stopping of the
        GeckoDriver."""
    
        def __init__(self, executable_path: str = DEFAULT_EXECUTABLE_PATH,
                     port: int = 0, service_args: List[str] = None,
                     log_path: str = "geckodriver.log", env: dict = None):
    
  • 可以看到,这里有个参数log_path,其默认值是"geckodriver.log"

  • 那我们就可以这样修改了

    from selenium import webdriver
    from selenium.webdriver.firefox.service import Service
    # 听你的,传递一个Service对象
    # Service(log_path='') 就是一个
    driver = webdriver.Firefox(service=Service(log_path=''))
    
  • 至此DeprecationWarning: service_log_path has been deprecated, please pass in a Service object的这段没了

  • 不过下面的1678945260755 geckodriver INFO Listening on 127.0.0.1:11634,这样的仍然存在的

  • 而这部分实际是geckodriver这个驱动的打印信息

    C:\Users\xxx>geckodriver
    1678945567307   geckodriver     INFO    Listening on 127.0.0.1:4444
    
  • 如果要去掉,就是让geckodriver这个程序静默

  • 没有找到太好的办法,geckodriver的帮助如下

    C:\Users\xxx>geckodriver --help
    geckodriver 0.30.0 (d372710b98a6 2021-09-16 10:29 +0300)
    WebDriver implementation for Firefox
    
    USAGE:
        geckodriver [FLAGS] [OPTIONS]
    
    FLAGS:
            --connect-existing    Connect to an existing Firefox instance
        -h, --help                Prints this message
            --jsdebugger          Attach browser toolbox debugger for Firefox
        -v                        Log level verbosity (-v for debug and -vv for trace level)
        -V, --version             Prints version and copying information
    
    OPTIONS:
            --android-storage <ANDROID_STORAGE>    Selects storage location to be used for test data (deprecated). [possible
                                                   values: auto, app, internal, sdcard]
        -b, --binary <BINARY>                      Path to the Firefox binary
            --log <LEVEL>                          Set Gecko log level [possible values: fatal, error, warn, info, config,
                                                   debug, trace]
            --marionette-host <HOST>               Host to use to connect to Gecko [default: 127.0.0.1]
            --marionette-port <PORT>               Port to use to connect to Gecko [default: system-allocated port]
            --host <HOST>                          Host IP to use for WebDriver server [default: 127.0.0.1]
        -p, --port <PORT>                          Port to use for WebDriver server [default: 4444]
            --websocket-port <PORT>                Port to use to connect to WebDriver BiDi [default: 9222]
    
  • --log 这个参数可以让它控制输出级别

  • 最终我们可以这样写

    from selenium import webdriver
    from selenium.webdriver.firefox.service import Service
    
    driver = webdriver.Firefox(service=Service(log_path='',
                        service_args=['--log','fatal']))  # 你不能 ['--log fatal'] 会报错
    
    
  • 然,即便如此,控制台一样有打印

    Dynamically enable window occlusion 0
    WebDriver BiDi listening on ws://127.0.0.1:12166
    console.warn: SearchSettings: "get: No settings file exists, new profile?" (new NotFoundError("Could not open the file at C:\\Users\\SONGQI~1\\AppData\\Local\\Temp\\rust_mozprofiledrxhm5\\search.json.mozlz4", (void 0)))
    DevTools listening on ws://127.0.0.1:12166/devtools/browser/22a37523-84bc-4bd6-a5e9-79c30825d1e8
    
  • 啊~~~~~~~~~无语了,不弄了,放弃了,干点有意义的事情吧,有点无趣

〇、简介

MD5 是
哈希算法(散列算法)
的一种应用。Hash 算法虽然被称为算法,但实际上它
更像是一种思想
。Hash 算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是 Hash 算法。

算法目的就是,把
任意长度的输入
(又叫做预映射 pre-image),通过散列算法
变换成固定长度的输出
,该输出就是散列值。

注意,不同的输入可能会散列成相同的输出,所以不能从散列值来确定唯一的输入值。

散列函数简单的说就是:一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

Hash 算法是一个广义的算法,也可以认为是一种思想,使用 Hash 算法可以提高存储空间的利用率,可以提高数据的查询效率,因为很难找到其逆向规律,也可以做数字签名来保障数据传递的安全性。所以Hash算法被广泛地应用在互联网应用中。

散列值不同 => 原始值不同

如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。

散列值相同 ≠> 原始值相同

散列函数的输入和输出不是一一对应的,
如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等
(可能出现
哈希碰撞
)。

关于“撞库”(Credential Stuffing Attack)

其在网络安全中是一个古老的概念,按中文的字面意思解读,就是“碰撞数据库”的意思。“碰撞”意味着碰运气,即不一定能成功;而“数据库”中往往存储着大量敏感数据,比如我们登录一个网站所需要的用户名、密码,再比如手机号、身份证号等个人隐私信息。“撞库”在英文中的表述为 Credential Stuffing(密码嗅探),也非常直白的说明了撞库的主要场景:试图获取正确的账号/密码组合,大白话就是“盗号”。

二、C# 代码实现

根据传入参数,返回分大小写的 16 位或 32 位密文,并且可自定义编码规则。

// 测试
string jiamihou16 = SecurityMD5.MD5Encrypt("TestString", 16, false); // 8828701f97fa4511
string jiamihou32 = SecurityMD5.MD5Encrypt("TestString", 32);// 5B56F40F8828701F97FA4511DDCD25FB
/// <summary>
/// MD5 加密方法
/// </summary>
/// <param name="md5instr">待加密字符串</param>
/// <param name="digit">位数:16/32/64</param>
/// <param name="isupper">输出大小写:true 大写;false 小写(返回为 64 位时不区分大小写)</param>
/// <param name="encoding">字符编码规则,为空默认:UTF8</param>
/// <returns></returns>
public static string MD5Encrypt(string md5instr, int digit, bool isupper = true, Encoding encoding = null)
{
    string md5outstr = string.Empty;
    if (encoding == null)
        encoding = Encoding.UTF8;
    switch (digit)
    {
        case 16:
            // SecurityMD5.MD5Encrypt(md5instr, 16, true); // 16位大写
            // SecurityMD5.MD5Encrypt(md5instr, 16, false); // 16位小写
            var md5provider = new MD5CryptoServiceProvider();
            var hashinstr16 = md5provider.ComputeHash(encoding.GetBytes(md5instr));
            md5outstr = BitConverter.ToString(hashinstr16, 4, 8);
            md5outstr = md5outstr.Replace("-", "");
            if (!isupper)
                md5outstr = md5outstr.ToLower();
            return md5outstr;
        case 32:
            // SecurityMD5.MD5Encrypt(md5instr, 32, true); // 32位大写
            // SecurityMD5.MD5Encrypt(md5instr, 32, false); // 32位小写
            MD5 md532 = MD5.Create();
            byte[] hashinstr32 = md532.ComputeHash(encoding.GetBytes(md5instr));
            string upperorlowerflag = isupper ? "X2" : "x2";
            for (int i = 0; i < hashinstr32.Length; i++)
            {
                md5outstr = md5outstr + hashinstr32[i].ToString(upperorlowerflag);
            }
            return md5outstr;
        case 64:
            // SecurityMD5.MD5Encrypt(md5instr, 64); // 64位加密,加密后为24位的值,例如:9GnLVZEzFmZLMj963TqUEQ==
            MD5 md564 = MD5.Create();
            byte[] hashinstr64 = md564.ComputeHash(encoding.GetBytes(md5instr));
            return Convert.ToBase64String(hashinstr64);
        default:
            return "";
    }
}

三、js 代码实现

1/2 通过 crypto-js.js 实现

此方法调用比较简单,但是只能获取 32 位的密文。

// 先引入 js 文件
<script src="http://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/crypto-js.js"></script>
// npm(Node.js package manager)方式引入
> npm install crypto-js

// 直接调用即可
var jiemihou = CryptoJS.MD5("TestString"); // 57fe567eaa866373f851a526f07d9e26
var jiemihoudx = CryptoJS.MD5("TestString").toString().toUpperCase(); // 5B56F40F8828701F97FA4511DDCD25FB

2/2 通过 js 方法实现

根据传入参数,返回分大小写的 16 位或 32 位密文。

// 测试
    md5_function("TestString",16,false)
    >'8828701f97fa4511'
    md5_function("TestString",32,true)
    >'5B56F40F8828701F97FA4511DDCD25FB'

// MD5 加密(入参分别是:待加密内容、返回位数、是否为大写)
function md5_function(md5instr, digit, isupper){
    function md5_RotateLeft(lValue, iShiftBits) {
        return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
    }
    function md5_AddUnsigned(lX, lY) {
        var lX4, lY4, lX8, lY8, lResult;
        lX8 = (lX & 0x80000000);
        lY8 = (lY & 0x80000000);
        lX4 = (lX & 0x40000000);
        lY4 = (lY & 0x40000000);
        lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
        if (lX4 & lY4) {
            return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
        }
        if (lX4 | lY4) {
            if (lResult & 0x40000000) {
                return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
            } else {
                return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
            }
        } else {
            return (lResult ^ lX8 ^ lY8);
        }
    }
    function md5_F(x, y, z) {
        return (x & y) | ((~x) & z);
    }
    function md5_G(x, y, z) {
        return (x & z) | (y & (~z));
    }
    function md5_H(x, y, z) {
        return (x ^ y ^ z);
    }
    function md5_I(x, y, z) {
        return (y ^ (x | (~z)));
    }
    function md5_FF(a, b, c, d, x, s, ac) {
        a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_F(b, c, d), x), ac));
        return md5_AddUnsigned(md5_RotateLeft(a, s), b);
    };
    function md5_GG(a, b, c, d, x, s, ac) {
        a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_G(b, c, d), x), ac));
        return md5_AddUnsigned(md5_RotateLeft(a, s), b);
    };
    function md5_HH(a, b, c, d, x, s, ac) {
        a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_H(b, c, d), x), ac));
        return md5_AddUnsigned(md5_RotateLeft(a, s), b);
    };
    function md5_II(a, b, c, d, x, s, ac) {
        a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_I(b, c, d), x), ac));
        return md5_AddUnsigned(md5_RotateLeft(a, s), b);
    };
    function md5_ConvertToWordArray(md5instr) {
        var lWordCount;
        var lMessageLength = md5instr.length;
        var lNumberOfWords_temp1 = lMessageLength + 8;
        var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
        var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
        var lWordArray = Array(lNumberOfWords - 1);
        var lBytePosition = 0;
        var lByteCount = 0;
        while (lByteCount < lMessageLength) {
            lWordCount = (lByteCount - (lByteCount % 4)) / 4;
            lBytePosition = (lByteCount % 4) * 8;
            lWordArray[lWordCount] = (lWordArray[lWordCount] | (md5instr.charCodeAt(lByteCount) << lBytePosition));
            lByteCount++;
        }
        lWordCount = (lByteCount - (lByteCount % 4)) / 4;
        lBytePosition = (lByteCount % 4) * 8;
        lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
        lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
        lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
        return lWordArray;
    };
    function md5_WordToHex(lValue) {
        var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
        for (lCount = 0; lCount <= 3; lCount++) {
            lByte = (lValue >>> (lCount * 8)) & 255;
            WordToHexValue_temp = "0" + lByte.toString(16);
            WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
        }
        return WordToHexValue;
    };
    function md5_Utf8Encode(md5instr) {
        md5instr = md5instr?.replace(/\r\n/g, "\n");
        var utftext = "";
        for (var n = 0; n < md5instr.length; n++) {
            var c = md5instr.charCodeAt(n);
            if (c < 128) {
                utftext += String.fromCharCode(c);
            } else if ((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            } else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }
        }
        return utftext;
    };
    var x = Array();
    var k, AA, BB, CC, DD, a, b, c, d;
    var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
    var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
    var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
    var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
    md5instr = md5_Utf8Encode(md5instr);
    x = md5_ConvertToWordArray(md5instr);
    a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
    for (k = 0; k < x.length; k += 16) {
        AA = a; BB = b; CC = c; DD = d;
        a = md5_FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
        d = md5_FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
        c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
        b = md5_FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
        a = md5_FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
        d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
        c = md5_FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
        b = md5_FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
        a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
        d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
        c = md5_FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
        b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
        a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
        d = md5_FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
        c = md5_FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
        b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
        a = md5_GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
        d = md5_GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
        c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
        b = md5_GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
        a = md5_GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
        d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453);
        c = md5_GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
        b = md5_GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
        a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
        d = md5_GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
        c = md5_GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
        b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
        a = md5_GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
        d = md5_GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
        c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
        b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
        a = md5_HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
        d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
        c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
        b = md5_HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
        a = md5_HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
        d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
        c = md5_HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
        b = md5_HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
        a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
        d = md5_HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
        c = md5_HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
        b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
        a = md5_HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
        d = md5_HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
        c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
        b = md5_HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
        a = md5_II(a, b, c, d, x[k + 0], S41, 0xF4292244);
        d = md5_II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
        c = md5_II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
        b = md5_II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
        a = md5_II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
        d = md5_II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
        c = md5_II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
        b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
        a = md5_II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
        d = md5_II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
        c = md5_II(c, d, a, b, x[k + 6], S43, 0xA3014314);
        b = md5_II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
        a = md5_II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
        d = md5_II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
        c = md5_II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
        b = md5_II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
        a = md5_AddUnsigned(a, AA);
        b = md5_AddUnsigned(b, BB);
        c = md5_AddUnsigned(c, CC);
        d = md5_AddUnsigned(d, DD);
    }
    if(digit == 16){
        if(isupper)
            return (md5_WordToHex(b) + md5_WordToHex(c)).toUpperCase();
        else
            return (md5_WordToHex(b) + md5_WordToHex(c)).toLowerCase();
    }
    else if (digit == 32){
        if(isupper)
            return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toUpperCase();
        else
            return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toLowerCase();
    }
    return "";
}

问题发现

早上过来,饭都没来的及吃,运维就给我发来信息,说是某个接口调用大量超时。因为最近这个接口调用量是翻倍了,所以我就去检查了下慢SQL,发现确实是有较多的慢SQL,所以我就缩减了查询的时间范围,但是效果并不好。
过了一会发现,这个服务fullGC是有问题的,太频繁了,这个应该是导致接口超时的根本问题,因为时间也是对的上的。
这个是最近三个小时fullGC的监控图:
0
这个是最近三天fullGC的监控图:
0
对比一下,就不难发现,fullGC数量是从3月15号晚上9点开始增加的,也是这个接口对外开放的时间。

解决思路

1、首先去服务器上面下载dump文件,分析是哪里造成了内存泄漏,频繁触发fullGC。首先找出服务器内java文件的PID,然后保存dump文件,我们公司java服务是固定端口号:1

使用top命令:
0
然后执行命令:jmap -dump:file=202303160924.dump,format=b 1 ,保存dump文件
0

2、根据dump文件,分析出堆内对象的分布情况

    • 下载一个可以分析dump文件的工具,这里我下载是Jprofiler
    • 查看大对象的分析,发现是java.lang.ApplicationShutdownHooks的hooks占用太大内存,并且得知改熟悉是一个Map
0
    • 分析这个Map里面的元素引用关系,可以看到这个map里面存的都是线程对象,并且大部分都是一个名为java.util.concurrent.ScheduledThreadPoolExecutor@59648a61的线程池对象,到了这里就定位到问题代码了,是这次新加的接口里面有一个异步操作,用的guava并发包里面的一个超时等待功能的接口,具体思路就是启用一个定时任务线程池去定时去检查在规定时间内,是否返回结果。
0

3、看看我的业务代码是哪里出现了问题

//异步执行某个查询方法,并且在规定时间内返回查询结果
public <T> T asyncWithTimeout(ScheduledThreadPoolExecutor executor, Callable<T> callable,
                              long time, TimeUnit unit) {
    try {
        ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(threadPoolExecutor);
        ListenableFuture<T> future = listeningExecutorService.submit(callable);
        //这里是创建一个定时任务线程,去定时检查是否在规定时间内查询完毕,应该就是这个去添加了钩子函数,进去看看
        ScheduledExecutorService scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService(executor);
        return Futures.withTimeout(future, time, unit, scheduledExecutorService).get(time, unit);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        log.warn("异步方法执行失败,error:{}", e.getMessage());
    }
    return null;
}

//=======================guava并发包代码=======================

@Beta
@GwtIncompatible // TODO
public static ScheduledExecutorService getExitingScheduledExecutorService(
    ScheduledThreadPoolExecutor executor) {
  //每次都去创建一个新的对象
  return new Application().getExitingScheduledExecutorService(executor);
}

final ScheduledExecutorService getExitingScheduledExecutorService(
    ScheduledThreadPoolExecutor executor) {
  return getExitingScheduledExecutorService(executor, 120, TimeUnit.SECONDS);
}

final ScheduledExecutorService getExitingScheduledExecutorService(
    ScheduledThreadPoolExecutor executor, long terminationTimeout, TimeUnit timeUnit) {
  useDaemonThreadFactory(executor);
  ScheduledExecutorService service = Executors.unconfigurableScheduledExecutorService(executor);
  //添加构造函数的地方,进去看看
  addDelayedShutdownHook(executor, terminationTimeout, timeUnit);
  return service;
}

final void addDelayedShutdownHook(
    final ExecutorService service, final long terminationTimeout, final TimeUnit timeUnit) {
  checkNotNull(service);
  checkNotNull(timeUnit);
  //继续点进去
  addShutdownHook(
      MoreExecutors.newThread(
          //线程名字对上了,就在对象引用的截图里面出现过
          "DelayedShutdownHook-for-" + service,
          new Runnable() {
            @Override
            public void run() {
              try {
                // We'd like to log progress and failures that may arise in the
                // following code, but unfortunately the behavior of logging
                // is undefined in shutdown hooks.
                // This is because the logging code installs a shutdown hook of its
                // own. See Cleaner class inside {@link LogManager}.
                service.shutdown();
                service.awaitTermination(terminationTimeout, timeUnit);
              } catch (InterruptedException ignored) {
                // We're shutting down anyway, so just ignore.
              }
            }
          }));
}

@VisibleForTesting
void addShutdownHook(Thread hook) {
  Runtime.getRuntime().addShutdownHook(hook);
}

//=======================guava并发包代码=======================

public void addShutdownHook(Thread hook) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new RuntimePermission("shutdownHooks"));
    }
    //定位到问题了,就是这里添加的钩子函数
    ApplicationShutdownHooks.add(hook);
}

static synchronized void add(Thread hook) {
    if(hooks == null)
        throw new IllegalStateException("Shutdown in progress");

    if (hook.isAlive())
        throw new IllegalArgumentException("Hook already running");

    if (hooks.containsKey(hook))
        throw new IllegalArgumentException("Hook previously registered");

    //存在到 hooks 这个map对象里面,就是这个大对象
    hooks.put(hook, hook);
}

问题解决

经过上面问题的排查,造成hooks大对象的原因找到了,就是每次调用接口的时候,都会往hooks里面put一个对象。
所以,解决办法很简单,就是不用每次都去生成一个ScheduledExecutorService对象,类初始化的时候创建一次就行了
改造后的代码如下:
private ListeningExecutorService listeningExecutorService;
private ScheduledExecutorService scheduledExecutorService;

public static AsyncUtils getInstance() {
    return ThreadHolder.INSTANCE.getAsyncWithCallback();
}

@SuppressWarnings("UnstableApiUsage")
private AsyncUtils() {
    listeningExecutorService = MoreExecutors.listeningDecorator(ThreadPoolConstant.THREAD_POOL_EXECUTOR);
    scheduledExecutorService = MoreExecutors.getExitingScheduledExecutorService(ThreadPoolConstant.SCHEDULED_THREAD_POOL_EXECUTOR);
}

@SuppressWarnings("UnstableApiUsage")
public <T> T asyncWithTimeout(Callable<T> callable,
                              long time, TimeUnit unit) {
    try {
        ListenableFuture<T> future = listeningExecutorService.submit(callable);
        return Futures.withTimeout(future, time, unit, scheduledExecutorService).get(time, unit);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        log.warn("异步方法执行失败,error:{}", e.getMessage());
    }
    return null;
}

private enum ThreadHolder {
    /**
     * 线程持有类 INSTANCE
     */
    INSTANCE;
    private final AsyncUtils asyncUtils;

    ThreadHolder() {
        asyncUtils = new AsyncUtils();
    }

    public AsyncUtils getAsyncWithCallback() {
        return asyncUtils;
    }
}

最近 Kakao Brain 在 Hugging Face 发布了一个全新的开源图像文本数据集 COYO,包含 7 亿对图像和文本,并训练了两个新的视觉语言模型 ViT 和 ALIGN
ViT

ALIGN

这是 ALIGN 模型首次公开发布供开源使用,同时 ViT 和 ALIGN 模型的发布都附带有训练数据集。

Google 的 ViT 和 ALIGN 模型都使用了巨大的数据集 (ViT 训练于 3 亿张图像,ALIGN 训练于 18 亿个图像 - 文本对) 进行训练,因为数据集不公开导致无法复现。Kakao Brain 的 ViT 和 ALIGN 模型采用与 Google 原始模型相同的架构和超参数,不同的是其在开源
COYO
数据集上进行训练。对于想要拥有数据并复现视觉语言模型的研究人员有很大的价值。详细的 Kakao ViT 和 ALIGN 模型信息可以参照:

这篇博客将介绍新的
COYO
数据集、Kakao Brain 的 ViT 和 ALIGN 模型,以及如何使用它们!以下是主要要点:

  • 第一个开源的 ALIGN 模型!
  • 第一个在开源数据集
    COYO
    上训练的开源 ViT 和 ALIGN 模型。
  • Kakao Brain 的 ViT 和 ALIGN 模型表现与 Google 版本相当。
  • ViT 模型在 HF 上可演示!您可以使用自己的图像样本在线体验 ViT!

性能比较

Kakao Brain 发布的 ViT 和 ALIGN 模型与 Google 的模型表现相当,某些方面甚至更好。Kakao Brain 的 ALIGN-B7-Base 模型虽然训练的数据对少得多 ( 7 亿 VS 1.8 亿),但在图像 KNN 分类任务上表现与 Google 的 ALIGN-B7-Base 相当,在 MS-COCO 图像 - 文本检索、文本 - 图像检索任务上表现更好。Kakao Brain 的 ViT-L/16 在 384×512 的 ImageNet 和 ImageNet-ReaL 数据上的表现与 Google 的 ViT-L/16 相当。这意味着同行可以使用 Kakao Brain 的 ViT 和 ALIGN 模型来复现 Google 的 ViT 和 ALIGN ,尤其是当用户需要训练数据时。所以我们很高兴开源这些与现有技术相当的模型!

COYO 数据集

本次发布的模型特别之处在于都是基于开源的 COYO 数据集训练的。
COYO
数据集包含 7 亿图像 - 文本对,类似于 Google 的
ALIGN 1.8B
图像 - 文本数据集,是从网页上收集的“嘈杂”的 html 文本 (alt-text) 和图像对。
COYO-700M

ALIGN 1.8B
都是“嘈杂”的,只使用了适当的清洗处理。
COYO
类似于另一个开源的图像–文本数据集
LAION
,但有一些区别。尽管
LAION 2B
是一个更大的数据集,包含 20 亿个英语配对,但
COYO
的附带有更多元数据,为用户提供更多灵活性和更细粒度的使用。以下表格显示了它们之间的区别:
COYO
所有数据对都提供了美感评分,更健壮的水印评分和面部计数信息 (face count data)。

COYO LAION 2B ALIGN 1.8B
Image-text similarity score calculated with CLIP ViT-B/32 and ViT-L/14 models, they are provided as metadata but nothing is filtered out so as to avoid possible elimination bias Image-text similarity score provided with CLIP (ViT-B/32) - only examples above threshold 0.28 Minimal, Frequency based filtering
NSFW filtering on images and text NSFW filtering on images Google Cloud API
Face recognition (face count) data provided as meta-data No face recognition data NA
700 million pairs all English 2 billion English 1.8 billion
From CC 2020 Oct - 2021 Aug From CC 2014-2020 NA
Aesthetic Score Aesthetic Score Partial NA
More robust Watermark score Watermark Score NA
Hugging Face Hub Hugging Face Hub Not made public
English English English?

ViT 和 ALIGN 是如何工作的

这些模型是干什么的?让我们简要讨论一下 ViT 和 ALIGN 模型的工作原理。

ViT—Vision Transformer 是谷歌于 2020 年提出的一种视觉模型,类似于文本 Transformer 架构。这是一种与卷积神经网络不同的视觉方法 (AlexNet 自 2012 年以来一直主导视觉任务)。同样表现下,它的计算效率比 CNN 高达四倍,且具有域不可知性 (domain agnostic)。ViT 将输入的图像分解成一系列图像块 (patch),就像文本 Transformer 输入文本序列一样,然后为每个块提供位置嵌入以学习图像结构。ViT 的性能尤其在于具有出色的性能 - 计算权衡。谷歌的一些 ViT 模型是开源的,但其训练使用的 JFT-300 百万图像 - 标签对数据集尚未公开发布。Kakao Brain 的训练模型是基于公开发布的 COYO-Labeled-300M 进行训练,对应的 ViT 模型在各种任务上具有相似表现,其代码、模型和训练数据 (COYO-Labeled-300M) 完全公开,以便能够进行复现和科学研究。

谷歌在 2021 年推出了 ALIGN,它是一种基于“嘈杂”文本–图像数据训练的视觉语言模型,可用于各种视觉和跨模态任务,如文本 - 图像检索。ALIGN 采用简单的双编码器架构,通过对比损失函数学习图像和文本对,ALIGN 的“嘈杂”训练语料特点包括用语料规模弥补其噪音以及强大的鲁棒性。之前的视觉语言表示学习都是在手动标注的大规模数据集上进行训练,这就需要大量的预先处理和成本。ALIGN 的语料库使用 HTML 文本 (alt-text) 数据作为图像的描述,导致数据集不可避免地嘈杂,但更大的数据量 (18 亿对) 使 ALIGN 能够在各种任务上表现出 SoTA 水平。Kakao Brain 的模型是第一个 ALIGN 开源版本,它在 COYO 数据集上训练,表现比谷歌的结果更好。

如何使用 COYO 数据集


我们可以使用 Hugging Face

安装mysql

从mysql社区版的官方源去拉取镜像:
mysql/mysql-server - Docker Image | Docker Hub

docker run --name=mysql1 -d mysql/mysql-server:latest

镜像起来之后,mysql就默默的初始化好了,直接查看日志得到初始化密码

docker logs mysql1 2>&1 | grep GENERATED; //运行之后会得到GENERATED ROOT PASSWORD: Axegh3kAJyDLaRuBemecis&EShOs

进放容器,登陆mysql,输入下面代码之后,在密码框输入此,输入上面的显示的密码

docker exec -it mysql1 mysql -uroot -p

SQL命令重置密码

 ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';

充许root远程访问

update user set host='%' where user='root';

mysql 安装,配置,基本就结束了。

挂载数据卷

一般而言默认运行容器,数据会储存在容器内面,容器暂停,重启都不会影响容器内面的数据。如果容器被删除或重建那么将会丢失数据,合理的作法就是挂载数据卷,这样删除容器或者重建容器只需要重新挂裁数据卷就可以恢复如初。mysql默认情况在容器数据目标是:/var/lib/mysql

我们在母机根目标下创建一个文件夹:

mkdir mysqldata

挂载数据卷使用参数 -v

-v /mysqldata:/var/lib/mysql

-p 是做端口映射,方便远程访问,完整的语句是这样:

docker run --name=mysql1 -v /mysqldata:/var/lib/mysql -p 3306:3306 -d mysql/mysql-server:latest

定时备份

这里我们介绍官方自带的mysqldump,mysqldump命令在容器内面,无需单独安装。

登陆容器:

docker exec -it a8471e33b4e0 /bin/bash //a8471e33b4e0是容器ID,自行更换

进入容器之后运行mysqldump,就可以备份进行逻辑备份了。

mysqldump -uroot -p123456 --databases dbname >/mysqlbak/2023-3-15.sql

// -u后面是账号
// -p后面是密码
// --databases后面是数据库名
// /mysqlbak/2023-3-15.sql是备份路径和文件名

一般来说不需要进入容器也可以备份,完整指令如下:

docker exec -it a8471e33b4e0 mysqldump -uroot -p123456 --databases dbname > /mysqlbak/2023-3-15.sql

注意事项,默认备份下,备份数据会储存在容器内面所以容器被删除或重建之后会面临丢失问题,所以我们也需要挂载一个备份数据卷。

我们在母机根目标下创建一个备份文件夹:

mkdir mysqlbak

结合开头的数据卷文件夹,我们完整的运行mysql容器的命令应该是这样如下,容器路径不用担心,即使不存在也不会报错。当然别忘了--restart=always,容器自启动,这样重启之后也可以自己启动。

docker run --name=mysql1 -v /mysqldata:/var/lib/mysql -v /mysqlbak:/mysqlbak -p 3306:3306 --restart=always -d mysql/mysql-server:latest

我们使用母机crontab定时任务来实现定时备份功能:下面代码演示每格一分钟备份一次,
注意第一点在crontab内面需要添加'\'转义符才能使用%号,每二点docker不能带-'t',因为crontab不能给他一个终端,会报错

*/1 * * * * docker exec -i 7003ff7c37a7 mysqldump -uroot -p123456 --databases chat > /mysqlbak/$(date +\%Y-\%m-\%d-\%H-\%M-\%S).sql

恢复

可以使有mysql命令进行恢复

mysql -u username -P [dbname] < filename.sql

//username 表示用户名称。
//p 表示密码。
//dbname 表示数据库名称。
//filename.sql 表示备份文件的名称。

我们先使用docker 启动一个mysql容器,将备份文件夹启动的时候作为数据卷映射进去,例如这样:

docker run --name=mysql1 -v /mysqlbak:/mysqlbak -p 3306:3306 --restart=always -d mysql/mysql-server:latest

然后无需进行容器,执行命令恢复mysql

docker exec -it fda84173e7a3 mysql -uroot -p123456 < /mysqlbak/2023-03-16-11-26-02.sql

//fda84173e7a3 为容器ID