From 68aedb4a259e6f3bc4f4446ba0ec6a6912d4661c Mon Sep 17 00:00:00 2001 From: wangzhiwubigdata <2827873682@qq.com> Date: Sat, 4 Jan 2020 11:46:16 +0800 Subject: [PATCH] add notes --- ...0_\347\232\204\344\275\277\347\224\250.md" | 225 ++++++ ...0_\347\232\204\344\275\277\347\224\250.md" | 296 +++++++ .../Azkaban\347\256\200\344\273\213.md" | 76 ++ .../Flink_Data_Sink.md" | 268 ++++++ .../Flink_Data_Source.md" | 284 +++++++ .../Flink_Data_Transformation.md" | 311 +++++++ .../Flink_Windows.md" | 128 +++ ...57\345\242\203\346\220\255\345\273\272.md" | 304 +++++++ ...02\345\277\265\347\273\274\350\277\260.md" | 173 ++++ ...45\347\202\271\346\234\272\345\210\266.md" | 370 +++++++++ .../Flume\346\225\264\345\220\210Kafka.md" | 116 +++ ...72\346\234\254\344\275\277\347\224\250.md" | 375 +++++++++ .../HDFS-Java-API.md" | 388 +++++++++ ...7\224\250Shell\345\221\275\344\273\244.md" | 141 ++++ .../Hadoop-HDFS.md" | 176 ++++ .../Hadoop-MapReduce.md" | 384 +++++++++ .../Hadoop-YARN.md" | 128 +++ .../Hbase_Java_API.md" | 761 ++++++++++++++++++ .../Hbase_Shell.md" | 279 +++++++ ...06\345\231\250\350\257\246\350\247\243.md" | 490 +++++++++++ ...76\344\270\216\345\244\207\344\273\275.md" | 196 +++++ ...70\255\351\227\264\345\261\202_Phoenix.md" | 241 ++++++ .../Hbase\347\256\200\344\273\213.md" | 88 ++ ...60\346\215\256\347\273\223\346\236\204.md" | 222 +++++ ...44\345\231\250\350\257\246\350\247\243.md" | 445 ++++++++++ ...72\346\234\254\344\275\277\347\224\250.md" | 279 +++++++ ...14\345\210\206\346\241\266\350\241\250.md" | 168 ++++ ...347\224\250DDL\346\223\215\344\275\234.md" | 450 +++++++++++ ...347\224\250DML\346\223\215\344\275\234.md" | 329 ++++++++ ...45\350\257\242\350\257\246\350\247\243.md" | 396 +++++++++ ...70\345\277\203\346\246\202\345\277\265.md" | 202 +++++ ...76\345\222\214\347\264\242\345\274\225.md" | 236 ++++++ ...71\350\200\205\350\257\246\350\247\243.md" | 392 +++++++++ ...57\346\234\254\346\234\272\345\210\266.md" | 161 ++++ ...47\350\200\205\350\257\246\350\247\243.md" | 364 +++++++++ .../Kafka\347\256\200\344\273\213.md" | 67 ++ ...60\345\222\214\351\227\255\345\214\205.md" | 312 +++++++ ...27\350\241\250\345\222\214\351\233\206.md" | 542 +++++++++++++ ...14\350\277\220\347\256\227\347\254\246.md" | 274 +++++++ .../Scala\346\225\260\347\273\204.md" | 193 +++++ ...04\345\222\214\345\205\203\347\273\204.md" | 282 +++++++ ...41\345\274\217\345\214\271\351\205\215.md" | 172 ++++ ...47\345\210\266\350\257\255\345\217\245.md" | 211 +++++ ...57\345\242\203\351\205\215\347\275\256.md" | 133 +++ ...73\345\222\214\345\257\271\350\261\241.md" | 412 ++++++++++ ...73\345\236\213\345\217\202\346\225\260.md" | 467 +++++++++++ ...77\345\222\214\347\211\271\350\264\250.md" | 418 ++++++++++ ...20\345\274\217\345\217\202\346\225\260.md" | 356 ++++++++ ...06\345\220\210\347\261\273\345\236\213.md" | 259 ++++++ ...2\214DataFrame\347\256\200\344\273\213.md" | 147 ++++ ...50\346\225\260\346\215\256\346\272\220.md" | 499 ++++++++++++ ...32\345\220\210\345\207\275\346\225\260.md" | 339 ++++++++ ...24\347\273\223\346\223\215\344\275\234.md" | 185 +++++ .../Spark_RDD.md" | 237 ++++++ ...16\346\265\201\345\244\204\347\220\206.md" | 79 ++ ...72\346\234\254\346\223\215\344\275\234.md" | 335 ++++++++ ...Streaming\346\225\264\345\220\210Flume.md" | 359 +++++++++ ...Streaming\346\225\264\345\220\210Kafka.md" | 321 ++++++++ ...72\346\234\254\344\275\277\347\224\250.md" | 244 ++++++ ...\222\214Action\347\256\227\345\255\220.md" | 418 ++++++++++ .../Spark\347\256\200\344\273\213.md" | 94 +++ ...77\346\222\255\345\217\230\351\207\217.md" | 105 +++ ...34\344\270\232\346\217\220\344\272\244.md" | 248 ++++++ ...ybtais+Phoenix\346\225\264\345\220\210.md" | 386 +++++++++ ...72\346\234\254\344\275\277\347\224\250.md" | 387 +++++++++ ...13\344\270\216\345\256\211\350\243\205.md" | 147 ++++ ...71\346\257\224\345\210\206\346\236\220.md" | 315 ++++++++ ...04\347\220\206\347\256\200\344\273\213.md" | 98 +++ ...02\345\277\265\350\257\246\350\247\243.md" | 159 ++++ ...41\345\236\213\350\257\246\350\247\243.md" | 511 ++++++++++++ ...3\206\346\210\220HBase\345\222\214HDFS.md" | 489 +++++++++++ .../Storm\351\233\206\346\210\220Kakfa.md" | 367 +++++++++ ...6\210\220Redis\350\257\246\350\247\243.md" | 655 +++++++++++++++ ...03\351\231\220\346\216\247\345\210\266.md" | 283 +++++++ ...256\242\346\210\267\347\253\257Curator.md" | 336 ++++++++ ...7\224\250Shell\345\221\275\344\273\244.md" | 265 ++++++ ...70\345\277\203\346\246\202\345\277\265.md" | 207 +++++ ...21\345\217\212\351\203\250\347\275\262.md" | 124 +++ .../installation/Flink_Standalone_Cluster.md" | 270 +++++++ ...57\345\242\203\346\220\255\345\273\272.md" | 227 ++++++ ...57\345\242\203\346\220\255\345\273\272.md" | 200 +++++ ...57\345\242\203\346\220\255\345\273\272.md" | 262 ++++++ ...57\345\242\203\346\220\255\345\273\272.md" | 233 ++++++ ...me\347\232\204\345\256\211\350\243\205.md" | 68 ++ ...344\270\213JDK\345\256\211\350\243\205.md" | 55 ++ ...\270\213Python\345\256\211\350\243\205.md" | 71 ++ ...11\350\243\205\351\203\250\347\275\262.md" | 181 +++++ ...57\345\242\203\346\220\255\345\273\272.md" | 178 ++++ ...57\345\242\203\346\220\255\345\273\272.md" | 190 +++++ ...57\345\242\203\346\220\255\345\273\272.md" | 81 ++ ...57\345\242\203\346\220\255\345\273\272.md" | 167 ++++ ...57\345\242\203\346\220\255\345\273\272.md" | 187 +++++ ...57\347\224\250\351\233\206\347\276\244.md" | 514 ++++++++++++ ...57\347\224\250\351\233\206\347\276\244.md" | 239 ++++++ ...\345\244\232IP\351\205\215\347\275\256.md" | 118 +++ ...46\344\271\240\350\267\257\347\272\277.md" | 169 ++++ ...11\350\243\205\346\214\207\345\215\227.md" | 67 ++ ...23\345\214\205\346\226\271\345\274\217.md" | 306 +++++++ ...35\347\273\264\345\257\274\345\233\276.md" | 2 + ...45\345\205\267\346\216\250\350\215\220.md" | 56 ++ 100 files changed, 26120 insertions(+) create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_1.0_\347\232\204\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_2.0_\347\232\204\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Sink.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Source.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Transformation.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Windows.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\346\240\270\345\277\203\346\246\202\345\277\265\347\273\274\350\277\260.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\347\212\266\346\200\201\347\256\241\347\220\206\344\270\216\346\243\200\346\237\245\347\202\271\346\234\272\345\210\266.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\346\225\264\345\220\210Kafka.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\347\256\200\344\273\213\345\217\212\345\237\272\346\234\254\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS-Java-API.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-HDFS.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-MapReduce.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-YARN.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Java_API.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Shell.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\215\217\345\244\204\347\220\206\345\231\250\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\256\271\347\201\276\344\270\216\345\244\207\344\273\275.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\232\204SQL\344\270\255\351\227\264\345\261\202_Phoenix.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\263\273\347\273\237\346\236\266\346\236\204\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\350\277\207\346\273\244\345\231\250\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HiveCLI\345\222\214Beeline\345\221\275\344\273\244\350\241\214\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\210\206\345\214\272\350\241\250\345\222\214\345\210\206\346\241\266\350\241\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DDL\346\223\215\344\275\234.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DML\346\223\215\344\275\234.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\346\225\260\346\215\256\346\237\245\350\257\242\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\266\210\350\264\271\350\200\205\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\267\261\345\205\245\347\220\206\350\247\243\345\210\206\345\214\272\345\211\257\346\234\254\346\234\272\345\210\266.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\224\237\344\272\247\350\200\205\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\207\275\346\225\260\345\222\214\351\227\255\345\214\205.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\210\227\350\241\250\345\222\214\351\233\206.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\345\222\214\350\277\220\347\256\227\347\254\246.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\225\260\347\273\204.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\230\240\345\260\204\345\222\214\345\205\203\347\273\204.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\250\241\345\274\217\345\214\271\351\205\215.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\265\201\347\250\213\346\216\247\345\210\266\350\257\255\345\217\245.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\256\200\344\273\213\345\217\212\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\222\214\345\257\271\350\261\241.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\236\213\345\217\202\346\225\260.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\273\247\346\211\277\345\222\214\347\211\271\350\264\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\232\220\345\274\217\350\275\254\346\215\242\345\222\214\351\232\220\345\274\217\345\217\202\346\225\260.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\233\206\345\220\210\347\261\273\345\236\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL_Dataset\345\222\214DataFrame\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\244\226\351\203\250\346\225\260\346\215\256\346\272\220.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\270\270\347\224\250\350\201\232\345\220\210\345\207\275\346\225\260.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\350\201\224\347\273\223\346\223\215\344\275\234.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_RDD.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\344\270\216\346\265\201\345\244\204\347\220\206.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\345\237\272\346\234\254\346\223\215\344\275\234.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Flume.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Kafka.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Structured_API\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Transformation\345\222\214Action\347\256\227\345\255\220.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\264\257\345\212\240\345\231\250\344\270\216\345\271\277\346\222\255\345\217\230\351\207\217.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\351\203\250\347\275\262\346\250\241\345\274\217\344\270\216\344\275\234\344\270\232\346\217\220\344\272\244.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spring+Mybtais+Phoenix\346\225\264\345\220\210.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\345\237\272\346\234\254\344\275\277\347\224\250.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\347\256\200\344\273\213\344\270\216\345\256\211\350\243\205.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\344\270\211\347\247\215\346\211\223\345\214\205\346\226\271\345\274\217\345\257\271\346\257\224\345\210\206\346\236\220.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\345\222\214\346\265\201\345\244\204\347\220\206\347\256\200\344\273\213.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\346\240\270\345\277\203\346\246\202\345\277\265\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\347\274\226\347\250\213\346\250\241\345\236\213\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220HBase\345\222\214HDFS.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Kakfa.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Redis\350\257\246\350\247\243.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_ACL\346\235\203\351\231\220\346\216\247\345\210\266.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_Java\345\256\242\346\210\267\347\253\257Curator.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Azkaban_3.x_\347\274\226\350\257\221\345\217\212\351\203\250\347\275\262.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Flink_Standalone_Cluster.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Flume\347\232\204\345\256\211\350\243\205.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213JDK\345\256\211\350\243\205.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Python\345\256\211\350\243\205.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\347\216\257\345\242\203\344\270\213Hive\347\232\204\345\256\211\350\243\205\351\203\250\347\275\262.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Zookeeper\345\215\225\346\234\272\347\216\257\345\242\203\345\222\214\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Hadoop\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Kafka\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\350\231\232\346\213\237\346\234\272\351\235\231\346\200\201IP\345\217\212\345\244\232IP\351\205\215\347\275\256.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240\350\267\257\347\272\277.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\270\270\347\224\250\350\275\257\344\273\266\345\256\211\350\243\205\346\214\207\345\215\227.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\272\224\347\224\250\345\270\270\347\224\250\346\211\223\345\214\205\346\226\271\345\274\217.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\346\212\200\346\234\257\346\240\210\346\200\235\347\273\264\345\257\274\345\233\276.md" create mode 100644 "\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\350\265\204\346\226\231\345\210\206\344\272\253\344\270\216\345\267\245\345\205\267\346\216\250\350\215\220.md" diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_1.0_\347\232\204\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_1.0_\347\232\204\344\275\277\347\224\250.md" new file mode 100644 index 0000000..11a6d5e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_1.0_\347\232\204\344\275\277\347\224\250.md" @@ -0,0 +1,225 @@ +# Azkaban Flow 1.0 的使用 + + + + + +## 一、简介 + +Azkaban 主要通过界面上传配置文件来进行任务的调度。它有两个重要的概念: + +- **Job**: 你需要执行的调度任务; +- **Flow**:一个获取多个 Job 及它们之间的依赖关系所组成的图表叫做 Flow。 + +目前 Azkaban 3.x 同时支持 Flow 1.0 和 Flow 2.0,本文主要讲解 Flow 1.0 的使用,下一篇文章会讲解 Flow 2.0 的使用。 + +## 二、基本任务调度 + +### 2.1 新建项目 + +在 Azkaban 主界面可以创建对应的项目: + +
+ +### 2.2 任务配置 + +新建任务配置文件 `Hello-Azkaban.job`,内容如下。这里的任务很简单,就是输出一句 `'Hello Azkaban!'` : + +```shell +#command.job +type=command +command=echo 'Hello Azkaban!' +``` + +### 2.3 打包上传 + +将 `Hello-Azkaban.job ` 打包为 `zip` 压缩文件: + +
+ +通过 Web UI 界面上传: + +
+ +上传成功后可以看到对应的 Flows: + +
+ +### 2.4 执行任务 + +点击页面上的 `Execute Flow` 执行任务: + +
+ +### 2.5 执行结果 + +点击 `detail` 可以查看到任务的执行日志: + +
+ +
+ +## 三、多任务调度 + +### 3.1 依赖配置 + +这里假设我们有五个任务(TaskA——TaskE),D 任务需要在 A,B,C 任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,这种情况下需要使用 `dependencies` 属性定义其依赖关系。各任务配置如下: + +**Task-A.job** : + +```shell +type=command +command=echo 'Task A' +``` + +**Task-B.job** : + +```shell +type=command +command=echo 'Task B' +``` + +**Task-C.job** : + +```shell +type=command +command=echo 'Task C' +``` + +**Task-D.job** : + +```shell +type=command +command=echo 'Task D' +dependencies=Task-A,Task-B,Task-C +``` + +**Task-E.job** : + +```shell +type=command +command=echo 'Task E' +dependencies=Task-D +``` + +### 3.2 压缩上传 + +压缩后进行上传,这里需要注意的是一个 Project 只能接收一个压缩包,这里我还沿用上面的 Project,默认后面的压缩包会覆盖前面的压缩包: + +
+ +### 3.3 依赖关系 + +多个任务存在依赖时,默认采用最后一个任务的文件名作为 Flow 的名称,其依赖关系如图: + +
+ +### 3.4 执行结果 + +
+ +从这个案例可以看出,Flow1.0 无法通过一个 job 文件来完成多个任务的配置,但是 Flow 2.0 就很好的解决了这个问题。 + +## 四、调度HDFS作业 + +步骤与上面的步骤一致,这里以查看 HDFS 上的文件列表为例。命令建议采用完整路径,配置文件如下: + +```shell +type=command +command=/usr/app/hadoop-2.6.0-cdh5.15.2/bin/hadoop fs -ls / +``` + +执行结果: + +
+ +## 五、调度MR作业 + +MR 作业配置: + +```shell +type=command +command=/usr/app/hadoop-2.6.0-cdh5.15.2/bin/hadoop jar /usr/app/hadoop-2.6.0-cdh5.15.2/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 +``` + +执行结果: + +
+ +## 六、调度Hive作业 + +作业配置: + +```shell +type=command +command=/usr/app/hive-1.1.0-cdh5.15.2/bin/hive -f 'test.sql' +``` + +其中 `test.sql` 内容如下,创建一张雇员表,然后查看其结构: + +```sql +CREATE DATABASE IF NOT EXISTS hive; +use hive; +drop table if exists emp; +CREATE TABLE emp( +empno int, +ename string, +job string, +mgr int, +hiredate string, +sal double, +comm double, +deptno int +) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'; +-- 查看 emp 表的信息 +desc emp; +``` + +打包的时候将 `job` 文件与 `sql` 文件一并进行打包: + +
+ +执行结果如下: + +
+ +## 七、在线修改作业配置 + +在测试时,我们可能需要频繁修改配置,如果每次修改都要重新打包上传,这会比较麻烦。所以 Azkaban 支持配置的在线修改,点击需要修改的 Flow,就可以进入详情页面: + +
+ +在详情页面点击 `Eidt` 按钮可以进入编辑页面: + +
+ +在编辑页面可以新增配置或者修改配置: + +
+ +## 附:可能出现的问题 + +如果出现以下异常,多半是因为执行主机内存不足,Azkaban 要求执行主机的可用内存必须大于 3G 才能执行任务: + +```shell +Cannot request memory (Xms 0 kb, Xmx 0 kb) from system for job +``` + +
+ +如果你的执行主机没办法增大内存,那么可以通过修改 `plugins/jobtypes/` 目录下的 `commonprivate.properties` 文件来关闭内存检查,配置如下: + +```shell +memCheck.enabled=false +``` + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_2.0_\347\232\204\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_2.0_\347\232\204\344\275\277\347\224\250.md" new file mode 100644 index 0000000..cfd236e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban_Flow_2.0_\347\232\204\344\275\277\347\224\250.md" @@ -0,0 +1,296 @@ +# Azkaban Flow 2.0的使用 + + + + +## 一、Flow 2.0 简介 + +### 1.1 Flow 2.0 的产生 + +Azkaban 目前同时支持 Flow 1.0 和 Flow2.0 ,但是官方文档上更推荐使用 Flow 2.0,因为 Flow 1.0 会在将来的版本被移除。Flow 2.0 的主要设计思想是提供 1.0 所没有的流级定义。用户可以将属于给定流的所有 `job / properties` 文件合并到单个流定义文件中,其内容采用 YAML 语法进行定义,同时还支持在流中再定义流,称为为嵌入流或子流。 + +### 1.2 基本结构 + +项目 zip 将包含多个流 YAML 文件,一个项目 YAML 文件以及可选库和源代码。Flow YAML 文件的基本结构如下: + ++ 每个 Flow 都在单个 YAML 文件中定义; ++ 流文件以流名称命名,如:`my-flow-name.flow`; ++ 包含 DAG 中的所有节点; ++ 每个节点可以是作业或流程; ++ 每个节点 可以拥有 name, type, config, dependsOn 和 nodes sections 等属性; ++ 通过列出 dependsOn 列表中的父节点来指定节点依赖性; ++ 包含与流相关的其他配置; ++ 当前 properties 文件中流的所有常见属性都将迁移到每个流 YAML 文件中的 config 部分。 + +官方提供了一个比较完善的配置样例,如下: + +```yaml +config: + user.to.proxy: azktest + param.hadoopOutData: /tmp/wordcounthadoopout + param.inData: /tmp/wordcountpigin + param.outData: /tmp/wordcountpigout + +# This section defines the list of jobs +# A node can be a job or a flow +# In this example, all nodes are jobs +nodes: + # Job definition + # The job definition is like a YAMLified version of properties file + # with one major difference. All custom properties are now clubbed together + # in a config section in the definition. + # The first line describes the name of the job + - name: AZTest + type: noop + # The dependsOn section contains the list of parent nodes the current + # node depends on + dependsOn: + - hadoopWC1 + - NoOpTest1 + - hive2 + - java1 + - jobCommand2 + + - name: pigWordCount1 + type: pig + # The config section contains custom arguments or parameters which are + # required by the job + config: + pig.script: src/main/pig/wordCountText.pig + + - name: hadoopWC1 + type: hadoopJava + dependsOn: + - pigWordCount1 + config: + classpath: ./* + force.output.overwrite: true + input.path: ${param.inData} + job.class: com.linkedin.wordcount.WordCount + main.args: ${param.inData} ${param.hadoopOutData} + output.path: ${param.hadoopOutData} + + - name: hive1 + type: hive + config: + hive.script: src/main/hive/showdb.q + + - name: NoOpTest1 + type: noop + + - name: hive2 + type: hive + dependsOn: + - hive1 + config: + hive.script: src/main/hive/showTables.sql + + - name: java1 + type: javaprocess + config: + Xms: 96M + java.class: com.linkedin.foo.HelloJavaProcessJob + + - name: jobCommand1 + type: command + config: + command: echo "hello world from job_command_1" + + - name: jobCommand2 + type: command + dependsOn: + - jobCommand1 + config: + command: echo "hello world from job_command_2" +``` + +## 二、YAML语法 + +想要使用 Flow 2.0 进行工作流的配置,首先需要了解 YAML 。YAML 是一种简洁的非标记语言,有着严格的格式要求的,如果你的格式配置失败,上传到 Azkaban 的时候就会抛出解析异常。 + +### 2.1 基本规则 + +1. 大小写敏感 ; +2. 使用缩进表示层级关系 ; +3. 缩进长度没有限制,只要元素对齐就表示这些元素属于一个层级; +4. 使用#表示注释 ; +5. 字符串默认不用加单双引号,但单引号和双引号都可以使用,双引号表示不需要对特殊字符进行转义; +6. YAML 中提供了多种常量结构,包括:整数,浮点数,字符串,NULL,日期,布尔,时间。 + +### 2.2 对象的写法 + +```yaml +# value 与 : 符号之间必须要有一个空格 +key: value +``` + +### 2.3 map的写法 + +```yaml +# 写法一 同一缩进的所有键值对属于一个map +key: + key1: value1 + key2: value2 + +# 写法二 +{key1: value1, key2: value2} +``` + +### 2.3 数组的写法 + +```yaml +# 写法一 使用一个短横线加一个空格代表一个数组项 +- a +- b +- c + +# 写法二 +[a,b,c] +``` + +### 2.5 单双引号 + +支持单引号和双引号,但双引号不会对特殊字符进行转义: + +```yaml +s1: '内容\n 字符串' +s2: "内容\n 字符串" + +转换后: +{ s1: '内容\\n 字符串', s2: '内容\n 字符串' } +``` + +### 2.6 特殊符号 + +一个 YAML 文件中可以包括多个文档,使用 `---` 进行分割。 + +### 2.7 配置引用 + +Flow 2.0 建议将公共参数定义在 `config` 下,并通过 `${}` 进行引用。 + + + +## 三、简单任务调度 + +### 3.1 任务配置 + +新建 `flow` 配置文件: + +```yaml +nodes: + - name: jobA + type: command + config: + command: echo "Hello Azkaban Flow 2.0." +``` + +在当前的版本中,Azkaban 同时支持 Flow 1.0 和 Flow 2.0,如果你希望以 2.0 的方式运行,则需要新建一个 `project` 文件,指明是使用的是 Flow 2.0: + +```shell +azkaban-flow-version: 2.0 +``` + +### 3.2 打包上传 + +
+ + + +### 3.3 执行结果 + +由于在 1.0 版本中已经介绍过 Web UI 的使用,这里就不再赘述。对于 1.0 和 2.0 版本,只有配置方式有所不同,其他上传执行的方式都是相同的。执行结果如下: + +
+ +## 四、多任务调度 + +和 1.0 给出的案例一样,这里假设我们有五个任务(jobA——jobE), D 任务需要在 A,B,C 任务执行完成后才能执行,而 E 任务则需要在 D 任务执行完成后才能执行,相关配置文件应如下。可以看到在 1.0 中我们需要分别定义五个配置文件,而在 2.0 中我们只需要一个配置文件即可完成配置。 + +```yaml +nodes: + - name: jobE + type: command + config: + command: echo "This is job E" + # jobE depends on jobD + dependsOn: + - jobD + + - name: jobD + type: command + config: + command: echo "This is job D" + # jobD depends on jobA、jobB、jobC + dependsOn: + - jobA + - jobB + - jobC + + - name: jobA + type: command + config: + command: echo "This is job A" + + - name: jobB + type: command + config: + command: echo "This is job B" + + - name: jobC + type: command + config: + command: echo "This is job C" +``` + +## 五、内嵌流 + +Flow2.0 支持在一个 Flow 中定义另一个 Flow,称为内嵌流或者子流。这里给出一个内嵌流的示例,其 `Flow` 配置如下: + +```yaml +nodes: + - name: jobC + type: command + config: + command: echo "This is job C" + dependsOn: + - embedded_flow + + - name: embedded_flow + type: flow + config: + prop: value + nodes: + - name: jobB + type: command + config: + command: echo "This is job B" + dependsOn: + - jobA + + - name: jobA + type: command + config: + command: echo "This is job A" +``` + +内嵌流的 DAG 图如下: + +
+ +执行情况如下: + +
+ + + +## 参考资料 + +1. [Azkaban Flow 2.0 Design](https://github.com/azkaban/azkaban/wiki/Azkaban-Flow-2.0-Design) +2. [Getting started with Azkaban Flow 2.0](https://github.com/azkaban/azkaban/wiki/Getting-started-with-Azkaban-Flow-2.0) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban\347\256\200\344\273\213.md" new file mode 100644 index 0000000..08b26fa --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Azkaban\347\256\200\344\273\213.md" @@ -0,0 +1,76 @@ +# Azkaban简介 + + +## 一、Azkaban 介绍 + +#### 1.1 背景 + +一个完整的大数据分析系统,必然由很多任务单元 (如数据收集、数据清洗、数据存储、数据分析等) 组成,所有的任务单元及其之间的依赖关系组成了复杂的工作流。复杂的工作流管理涉及到很多问题: + +- 如何定时调度某个任务? +- 如何在某个任务执行完成后再去执行另一个任务? +- 如何在任务失败时候发出预警? +- ...... + +面对这些问题,工作流调度系统应运而生。Azkaban 就是其中之一。 + +#### 1.2 功能 + +Azkaban 产生于 LinkedIn,并经过多年生产环境的检验,它具备以下功能: + +- 兼容任何版本的 Hadoop +- 易于使用的 Web UI +- 可以使用简单的 Web 页面进行工作流上传 +- 支持按项目进行独立管理 +- 定时任务调度 +- 模块化和可插入 +- 身份验证和授权 +- 跟踪用户操作 +- 支持失败和成功的电子邮件提醒 +- SLA 警报和自动查杀失败任务 +- 重试失败的任务 + +Azkaban 的设计理念是在保证功能实现的基础上兼顾易用性,其页面风格清晰明朗,下面是其 WEB UI 界面: + +
+ +## 二、Azkaban 和 Oozie + +Azkaban 和 Oozie 都是目前使用最为广泛的工作流调度程序,其主要区别如下: + +#### 功能对比 + +- 两者均可以调度 Linux 命令、MapReduce、Spark、Pig、Java、Hive 等工作流任务; +- 两者均可以定时执行工作流任务。 + +#### 工作流定义 + +- Azkaban 使用 Properties(Flow 1.0) 和 YAML(Flow 2.0) 文件定义工作流; +- Oozie 使用 Hadoop 流程定义语言(hadoop process defination language,HPDL)来描述工作流,HPDL 是一种 XML 流程定义语言。 + +#### 资源管理 + +- Azkaban 有较严格的权限控制,如用户对工作流进行读/写/执行等操作; +- Oozie 暂无严格的权限控制。 + +#### 运行模式 + ++ Azkaban 3.x 提供了两种运行模式: + + **solo server model(单服务模式)** :元数据默认存放在内置的 H2 数据库(可以修改为 MySQL),该模式中 `webServer`(管理服务器) 和 `executorServer`(执行服务器) 运行在同一个进程中,进程名是 `AzkabanSingleServer`。该模式适用于小规模工作流的调度。 + + **multiple-executor(分布式多服务模式)** :存放元数据的数据库为 MySQL,MySQL 应采用主从模式进行备份和容错。这种模式下 `webServer` 和 `executorServer` 在不同进程中运行,彼此之间互不影响,适合用于生产环境。 + ++ Oozie 使用 Tomcat 等 Web 容器来展示 Web 页面,默认使用 derby 存储工作流的元数据,由于 derby 过于轻量,实际使用中通常用 MySQL 代替。 + + + + + +## 三、总结 + +如果你的工作流不是特别复杂,推荐使用轻量级的 Azkaban,主要有以下原因: + ++ **安装方面**:Azkaban 3.0 之前都是提供安装包的,直接解压部署即可。Azkaban 3.0 之后的版本需要编译,这个编译是基于 gradle 的,自动化程度比较高; ++ **页面设计**:所有任务的依赖关系、执行结果、执行日志都可以从界面上直观查看到; ++ **配置方面**:Azkaban Flow 1.0 基于 Properties 文件来定义工作流,这个时候的限制可能会多一点。但是在 Flow 2.0 就支持了 YARM。YARM 语法更加灵活简单,著名的微服务框架 Spring Boot 就采用的 YAML 代替了繁重的 XML。 + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Sink.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Sink.md" new file mode 100644 index 0000000..12e5699 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Sink.md" @@ -0,0 +1,268 @@ +# Flink Sink + + + + +## 一、Data Sinks + +在使用 Flink 进行数据处理时,数据经 Data Source 流入,然后通过系列 Transformations 的转化,最终可以通过 Sink 将计算结果进行输出,Flink Data Sinks 就是用于定义数据流最终的输出位置。Flink 提供了几个较为简单的 Sink API 用于日常的开发,具体如下: + +### 1.1 writeAsText + +`writeAsText` 用于将计算结果以文本的方式并行地写入到指定文件夹下,除了路径参数是必选外,该方法还可以通过指定第二个参数来定义输出模式,它有以下两个可选值: + ++ **WriteMode.NO_OVERWRITE**:当指定路径上不存在任何文件时,才执行写出操作; ++ **WriteMode.OVERWRITE**:不论指定路径上是否存在文件,都执行写出操作;如果原来已有文件,则进行覆盖。 + +使用示例如下: + +```java + streamSource.writeAsText("D:\\out", FileSystem.WriteMode.OVERWRITE); +``` + +以上写出是以并行的方式写出到多个文件,如果想要将输出结果全部写出到一个文件,需要设置其并行度为 1: + +```java +streamSource.writeAsText("D:\\out", FileSystem.WriteMode.OVERWRITE).setParallelism(1); +``` + +### 1.2 writeAsCsv + +`writeAsCsv` 用于将计算结果以 CSV 的文件格式写出到指定目录,除了路径参数是必选外,该方法还支持传入输出模式,行分隔符,和字段分隔符三个额外的参数,其方法定义如下: + +```java +writeAsCsv(String path, WriteMode writeMode, String rowDelimiter, String fieldDelimiter) +``` + +### 1.3 print \ printToErr + +`print \ printToErr` 是测试当中最常用的方式,用于将计算结果以标准输出流或错误输出流的方式打印到控制台上。 + +### 1.4 writeUsingOutputFormat + +采用自定义的输出格式将计算结果写出,上面介绍的 `writeAsText` 和 `writeAsCsv` 其底层调用的都是该方法,源码如下: + +```java +public DataStreamSink writeAsText(String path, WriteMode writeMode) { + TextOutputFormat tof = new TextOutputFormat<>(new Path(path)); + tof.setWriteMode(writeMode); + return writeUsingOutputFormat(tof); +} +``` + +### 1.5 writeToSocket + +`writeToSocket` 用于将计算结果以指定的格式写出到 Socket 中,使用示例如下: + +```shell +streamSource.writeToSocket("192.168.0.226", 9999, new SimpleStringSchema()); +``` + + + +## 二、Streaming Connectors + +除了上述 API 外,Flink 中还内置了系列的 Connectors 连接器,用于将计算结果输入到常用的存储系统或者消息中间件中,具体如下: + +- Apache Kafka (支持 source 和 sink) +- Apache Cassandra (sink) +- Amazon Kinesis Streams (source/sink) +- Elasticsearch (sink) +- Hadoop FileSystem (sink) +- RabbitMQ (source/sink) +- Apache NiFi (source/sink) +- Google PubSub (source/sink) + +除了内置的连接器外,你还可以通过 Apache Bahir 的连接器扩展 Flink。Apache Bahir 旨在为分布式数据分析系统 (如 Spark,Flink) 等提供功能上的扩展,当前其支持的与 Flink Sink 相关的连接器如下: + +- Apache ActiveMQ (source/sink) +- Apache Flume (sink) +- Redis (sink) +- Akka (sink) + +这里接着在 Data Sources 章节介绍的整合 Kafka Source 的基础上,将 Kafka Sink 也一并进行整合,具体步骤如下。 + + + +## 三、整合 Kafka Sink + +### 3.1 addSink + +Flink 提供了 addSink 方法用来调用自定义的 Sink 或者第三方的连接器,想要将计算结果写出到 Kafka,需要使用该方法来调用 Kafka 的生产者 FlinkKafkaProducer,具体代码如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + +// 1.指定Kafka的相关配置属性 +Properties properties = new Properties(); +properties.setProperty("bootstrap.servers", "192.168.200.0:9092"); + +// 2.接收Kafka上的数据 +DataStream stream = env + .addSource(new FlinkKafkaConsumer<>("flink-stream-in-topic", new SimpleStringSchema(), properties)); + +// 3.定义计算结果到 Kafka ProducerRecord 的转换 +KafkaSerializationSchema kafkaSerializationSchema = new KafkaSerializationSchema() { + @Override + public ProducerRecord serialize(String element, @Nullable Long timestamp) { + return new ProducerRecord<>("flink-stream-out-topic", element.getBytes()); + } +}; +// 4. 定义Flink Kafka生产者 +FlinkKafkaProducer kafkaProducer = new FlinkKafkaProducer<>("flink-stream-out-topic", + kafkaSerializationSchema, + properties, + FlinkKafkaProducer.Semantic.AT_LEAST_ONCE, 5); +// 5. 将接收到输入元素*2后写出到Kafka +stream.map((MapFunction) value -> value + value).addSink(kafkaProducer); +env.execute("Flink Streaming"); +``` + +### 3.2 创建输出主题 + +创建用于输出测试的主题: + +```shell +bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 \ + --partitions 1 \ + --topic flink-stream-out-topic + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +### 3.3 启动消费者 + +启动一个 Kafka 消费者,用于查看 Flink 程序的输出情况: + +```java +bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flink-stream-out-topic +``` + +### 3.4 测试结果 + +在 Kafka 生产者上发送消息到 Flink 程序,观察 Flink 程序转换后的输出情况,具体如下: + +
+ + +可以看到 Kafka 生成者发出的数据已经被 Flink 程序正常接收到,并经过转换后又输出到 Kafka 对应的 Topic 上。 + +## 四、自定义 Sink + +除了使用内置的第三方连接器外,Flink 还支持使用自定义的 Sink 来满足多样化的输出需求。想要实现自定义的 Sink ,需要直接或者间接实现 SinkFunction 接口。通常情况下,我们都是实现其抽象类 RichSinkFunction,相比于 SinkFunction ,其提供了更多的与生命周期相关的方法。两者间的关系如下: + +
+ + +这里我们以自定义一个 FlinkToMySQLSink 为例,将计算结果写出到 MySQL 数据库中,具体步骤如下: + +### 4.1 导入依赖 + +首先需要导入 MySQL 相关的依赖: + +```xml + + mysql + mysql-connector-java + 8.0.16 + +``` + +### 4.2 自定义 Sink + +继承自 RichSinkFunction,实现自定义的 Sink : + +```java +public class FlinkToMySQLSink extends RichSinkFunction { + + private PreparedStatement stmt; + private Connection conn; + + @Override + public void open(Configuration parameters) throws Exception { + Class.forName("com.mysql.cj.jdbc.Driver"); + conn = DriverManager.getConnection("jdbc:mysql://192.168.0.229:3306/employees" + + "?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false", + "root", + "123456"); + String sql = "insert into emp(name, age, birthday) values(?, ?, ?)"; + stmt = conn.prepareStatement(sql); + } + + @Override + public void invoke(Employee value, Context context) throws Exception { + stmt.setString(1, value.getName()); + stmt.setInt(2, value.getAge()); + stmt.setDate(3, value.getBirthday()); + stmt.executeUpdate(); + } + + @Override + public void close() throws Exception { + super.close(); + if (stmt != null) { + stmt.close(); + } + if (conn != null) { + conn.close(); + } + } + +} +``` + +### 4.3 使用自定义 Sink + +想要使用自定义的 Sink,同样是需要调用 addSink 方法,具体如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +Date date = new Date(System.currentTimeMillis()); +DataStreamSource streamSource = env.fromElements( + new Employee("hei", 10, date), + new Employee("bai", 20, date), + new Employee("ying", 30, date)); +streamSource.addSink(new FlinkToMySQLSink()); +env.execute(); +``` + +### 4.4 测试结果 + +启动程序,观察数据库写入情况: + +
+ + +数据库成功写入,代表自定义 Sink 整合成功。 + +> 以上所有用例的源码见本仓库:[flink-kafka-integration]( https://github.com/heibaiying/BigData-Notes/tree/master/code/Flink/flink-kafka-integration) + + + +## 参考资料 + +1. data-sinks: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/datastream_api.html#data-sinks +2. Streaming Connectors:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html +3. Apache Kafka Connector: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Source.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Source.md" new file mode 100644 index 0000000..ebd70e5 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Source.md" @@ -0,0 +1,284 @@ +# Flink Data Source + + + + +## 一、内置 Data Source + +Flink Data Source 用于定义 Flink 程序的数据来源,Flink 官方提供了多种数据获取方法,用于帮助开发者简单快速地构建输入流,具体如下: + +### 1.1 基于文件构建 + +**1. readTextFile(path)**:按照 TextInputFormat 格式读取文本文件,并将其内容以字符串的形式返回。示例如下: + +```java +env.readTextFile(filePath).print(); +``` + +**2. readFile(fileInputFormat, path)** :按照指定格式读取文件。 + +**3. readFile(inputFormat, filePath, watchType, interval, typeInformation)**:按照指定格式周期性的读取文件。其中各个参数的含义如下: + ++ **inputFormat**:数据流的输入格式。 ++ **filePath**:文件路径,可以是本地文件系统上的路径,也可以是 HDFS 上的文件路径。 ++ **watchType**:读取方式,它有两个可选值,分别是 `FileProcessingMode.PROCESS_ONCE` 和 `FileProcessingMode.PROCESS_CONTINUOUSLY`:前者表示对指定路径上的数据只读取一次,然后退出;后者表示对路径进行定期地扫描和读取。需要注意的是如果 watchType 被设置为 `PROCESS_CONTINUOUSLY`,那么当文件被修改时,其所有的内容 (包含原有的内容和新增的内容) 都将被重新处理,因此这会打破 Flink 的 *exactly-once* 语义。 ++ **interval**:定期扫描的时间间隔。 ++ **typeInformation**:输入流中元素的类型。 + +使用示例如下: + +```java +final String filePath = "D:\\log4j.properties"; +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +env.readFile(new TextInputFormat(new Path(filePath)), + filePath, + FileProcessingMode.PROCESS_ONCE, + 1, + BasicTypeInfo.STRING_TYPE_INFO).print(); +env.execute(); +``` + +### 1.2 基于集合构建 + +**1. fromCollection(Collection)**:基于集合构建,集合中的所有元素必须是同一类型。示例如下: + +```java +env.fromCollection(Arrays.asList(1,2,3,4,5)).print(); +``` + +**2. fromElements(T ...)**: 基于元素构建,所有元素必须是同一类型。示例如下: + +```java +env.fromElements(1,2,3,4,5).print(); +``` +**3. generateSequence(from, to)**:基于给定的序列区间进行构建。示例如下: + +```java +env.generateSequence(0,100); +``` + +**4. fromCollection(Iterator, Class)**:基于迭代器进行构建。第一个参数用于定义迭代器,第二个参数用于定义输出元素的类型。使用示例如下: + +```java +env.fromCollection(new CustomIterator(), BasicTypeInfo.INT_TYPE_INFO).print(); +``` + +其中 CustomIterator 为自定义的迭代器,这里以产生 1 到 100 区间内的数据为例,源码如下。需要注意的是自定义迭代器除了要实现 Iterator 接口外,还必须要实现序列化接口 Serializable ,否则会抛出序列化失败的异常: + +```java +import java.io.Serializable; +import java.util.Iterator; + +public class CustomIterator implements Iterator, Serializable { + private Integer i = 0; + + @Override + public boolean hasNext() { + return i < 100; + } + + @Override + public Integer next() { + i++; + return i; + } +} +``` + +**5. fromParallelCollection(SplittableIterator, Class)**:方法接收两个参数,第二个参数用于定义输出元素的类型,第一个参数 SplittableIterator 是迭代器的抽象基类,它用于将原始迭代器的值拆分到多个不相交的迭代器中。 + +### 1.3 基于 Socket 构建 + +Flink 提供了 socketTextStream 方法用于构建基于 Socket 的数据流,socketTextStream 方法有以下四个主要参数: + +- **hostname**:主机名; +- **port**:端口号,设置为 0 时,表示端口号自动分配; +- **delimiter**:用于分隔每条记录的分隔符; +- **maxRetry**:当 Socket 临时关闭时,程序的最大重试间隔,单位为秒。设置为 0 时表示不进行重试;设置为负值则表示一直重试。示例如下: + +```shell + env.socketTextStream("192.168.0.229", 9999, "\n", 3).print(); +``` + + + +## 二、自定义 Data Source + +### 2.1 SourceFunction + +除了内置的数据源外,用户还可以使用 `addSource` 方法来添加自定义的数据源。自定义的数据源必须要实现 SourceFunction 接口,这里以产生 [0 , 1000) 区间内的数据为例,代码如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + +env.addSource(new SourceFunction() { + + private long count = 0L; + private volatile boolean isRunning = true; + + public void run(SourceContext ctx) { + while (isRunning && count < 1000) { + // 通过collect将输入发送出去 + ctx.collect(count); + count++; + } + } + + public void cancel() { + isRunning = false; + } + +}).print(); +env.execute(); +``` + +### 2.2 ParallelSourceFunction 和 RichParallelSourceFunction + +上面通过 SourceFunction 实现的数据源是不具有并行度的,即不支持在得到的 DataStream 上调用 `setParallelism(n)` 方法,此时会抛出如下的异常: + +```shell +Exception in thread "main" java.lang.IllegalArgumentException: Source: 1 is not a parallel source +``` + +如果你想要实现具有并行度的输入流,则需要实现 ParallelSourceFunction 或 RichParallelSourceFunction 接口,其与 SourceFunction 的关系如下图: + +
+ParallelSourceFunction 直接继承自 ParallelSourceFunction,具有并行度的功能。RichParallelSourceFunction 则继承自 AbstractRichFunction,同时实现了 ParallelSourceFunction 接口,所以其除了具有并行度的功能外,还提供了额外的与生命周期相关的方法,如 open() ,closen() 。 + +## 三、Streaming Connectors + +### 3.1 内置连接器 + +除了自定义数据源外, Flink 还内置了多种连接器,用于满足大多数的数据收集场景。当前内置连接器的支持情况如下: + +- Apache Kafka (支持 source 和 sink) +- Apache Cassandra (sink) +- Amazon Kinesis Streams (source/sink) +- Elasticsearch (sink) +- Hadoop FileSystem (sink) +- RabbitMQ (source/sink) +- Apache NiFi (source/sink) +- Twitter Streaming API (source) +- Google PubSub (source/sink) + +除了上述的连接器外,你还可以通过 Apache Bahir 的连接器扩展 Flink。Apache Bahir 旨在为分布式数据分析系统 (如 Spark,Flink) 等提供功能上的扩展,当前其支持的与 Flink 相关的连接器如下: + +- Apache ActiveMQ (source/sink) +- Apache Flume (sink) +- Redis (sink) +- Akka (sink) +- Netty (source) + +随着 Flink 的不断发展,可以预见到其会支持越来越多类型的连接器,关于连接器的后续发展情况,可以查看其官方文档:[Streaming Connectors]( https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html) 。在所有 DataSource 连接器中,使用的广泛的就是 Kafka,所以这里我们以其为例,来介绍 Connectors 的整合步骤。 + +### 3.2 整合 Kakfa + +#### 1. 导入依赖 + +整合 Kafka 时,一定要注意所使用的 Kafka 的版本,不同版本间所需的 Maven 依赖和开发时所调用的类均不相同,具体如下: + +| Maven 依赖 | Flink 版本 | Consumer and Producer 类的名称 | Kafka 版本 | +| :------------------------------ | :--------- | :----------------------------------------------- | :--------- | +| flink-connector-kafka-0.8_2.11 | 1.0.0 + | FlinkKafkaConsumer08
FlinkKafkaProducer08 | 0.8.x | +| flink-connector-kafka-0.9_2.11 | 1.0.0 + | FlinkKafkaConsumer09
FlinkKafkaProducer09 | 0.9.x | +| flink-connector-kafka-0.10_2.11 | 1.2.0 + | FlinkKafkaConsumer010
FlinkKafkaProducer010 | 0.10.x | +| flink-connector-kafka-0.11_2.11 | 1.4.0 + | FlinkKafkaConsumer011
FlinkKafkaProducer011 | 0.11.x | +| flink-connector-kafka_2.11 | 1.7.0 + | FlinkKafkaConsumer
FlinkKafkaProducer | >= 1.0.0 | + +这里我使用的 Kafka 版本为 kafka_2.12-2.2.0,添加的依赖如下: + +```xml + + org.apache.flink + flink-connector-kafka_2.11 + 1.9.0 + +``` + +#### 2. 代码开发 + +这里以最简单的场景为例,接收 Kafka 上的数据并打印,代码如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +Properties properties = new Properties(); +// 指定Kafka的连接位置 +properties.setProperty("bootstrap.servers", "hadoop001:9092"); +// 指定监听的主题,并定义Kafka字节消息到Flink对象之间的转换规则 +DataStream stream = env + .addSource(new FlinkKafkaConsumer<>("flink-stream-in-topic", new SimpleStringSchema(), properties)); +stream.print(); +env.execute("Flink Streaming"); +``` + +### 3.3 整合测试 + +#### 1. 启动 Kakfa + +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: + +```shell +# zookeeper启动命令 +bin/zkServer.sh start + +# 内置zookeeper启动命令 +bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动单节点 kafka 用于测试: + +```shell +# bin/kafka-server-start.sh config/server.properties +``` + +#### 2. 创建 Topic + +```shell +# 创建用于测试主题 +bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 \ + --partitions 1 \ + --topic flink-stream-in-topic + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +#### 3. 启动 Producer + +这里 启动一个 Kafka 生产者,用于发送测试数据: + +```shell +bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic flink-stream-in-topic +``` + +#### 4. 测试结果 + +在 Producer 上输入任意测试数据,之后观察程序控制台的输出: + +
+程序控制台的输出如下: + +
+可以看到已经成功接收并打印出相关的数据。 + + + +## 参考资料 + +1. data-sources:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/datastream_api.html#data-sources +2. Streaming Connectors:https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/index.html +3. Apache Kafka Connector: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/connectors/kafka.html diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Transformation.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Transformation.md" new file mode 100644 index 0000000..23c58bf --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Data_Transformation.md" @@ -0,0 +1,311 @@ +# Flink Transformation + + + + +## 一、Transformations 分类 + +Flink 的 Transformations 操作主要用于将一个和多个 DataStream 按需转换成新的 DataStream。它主要分为以下三类: + +- **DataStream Transformations**:进行数据流相关转换操作; +- **Physical partitioning**:物理分区。Flink 提供的底层 API ,允许用户定义数据的分区规则; +- **Task chaining and resource groups**:任务链和资源组。允许用户进行任务链和资源组的细粒度的控制。 + +以下分别对其主要 API 进行介绍: + +## 二、DataStream Transformations + +### 2.1 Map [DataStream → DataStream] + +对一个 DataStream 中的每个元素都执行特定的转换操作: + +```java +DataStream integerDataStream = env.fromElements(1, 2, 3, 4, 5); +integerDataStream.map((MapFunction) value -> value * 2).print(); +// 输出 2,4,6,8,10 +``` + +### 2.2 FlatMap [DataStream → DataStream] + +FlatMap 与 Map 类似,但是 FlatMap 中的一个输入元素可以被映射成一个或者多个输出元素,示例如下: + +```java +String string01 = "one one one two two"; +String string02 = "third third third four"; +DataStream stringDataStream = env.fromElements(string01, string02); +stringDataStream.flatMap(new FlatMapFunction() { + @Override + public void flatMap(String value, Collector out) throws Exception { + for (String s : value.split(" ")) { + out.collect(s); + } + } +}).print(); +// 输出每一个独立的单词,为节省排版,这里去掉换行,后文亦同 +one one one two two third third third four +``` + +### 2.3 Filter [DataStream → DataStream] + +用于过滤符合条件的数据: + +```java +env.fromElements(1, 2, 3, 4, 5).filter(x -> x > 3).print(); +``` + +### 2.4 KeyBy 和 Reduce + +- **KeyBy [DataStream → KeyedStream]** :用于将相同 Key 值的数据分到相同的分区中; +- **Reduce [KeyedStream → DataStream]** :用于对数据执行归约计算。 + +如下例子将数据按照 key 值分区后,滚动进行求和计算: + +```java +DataStream> tuple2DataStream = env.fromElements(new Tuple2<>("a", 1), + new Tuple2<>("a", 2), + new Tuple2<>("b", 3), + new Tuple2<>("b", 5)); +KeyedStream, Tuple> keyedStream = tuple2DataStream.keyBy(0); +keyedStream.reduce((ReduceFunction>) (value1, value2) -> + new Tuple2<>(value1.f0, value1.f1 + value2.f1)).print(); + +// 持续进行求和计算,输出: +(a,1) +(a,3) +(b,3) +(b,8) +``` + +KeyBy 操作存在以下两个限制: + +- KeyBy 操作用于用户自定义的 POJOs 类型时,该自定义类型必须重写 hashCode 方法; +- KeyBy 操作不能用于数组类型。 + +### 2.5 Aggregations [KeyedStream → DataStream] + +Aggregations 是官方提供的聚合算子,封装了常用的聚合操作,如上利用 Reduce 进行求和的操作也可以利用 Aggregations 中的 sum 算子重写为下面的形式: + +```java +tuple2DataStream.keyBy(0).sum(1).print(); +``` + +除了 sum 外,Flink 还提供了 min , max , minBy,maxBy 等常用聚合算子: + +```java +// 滚动计算指定key的最小值,可以通过index或者fieldName来指定key +keyedStream.min(0); +keyedStream.min("key"); +// 滚动计算指定key的最大值 +keyedStream.max(0); +keyedStream.max("key"); +// 滚动计算指定key的最小值,并返回其对应的元素 +keyedStream.minBy(0); +keyedStream.minBy("key"); +// 滚动计算指定key的最大值,并返回其对应的元素 +keyedStream.maxBy(0); +keyedStream.maxBy("key"); + +``` + +### 2.6 Union [DataStream* → DataStream] + +用于连接两个或者多个元素类型相同的 DataStream 。当然一个 DataStream 也可以与其本生进行连接,此时该 DataStream 中的每个元素都会被获取两次: + +```shell +DataStreamSource> streamSource01 = env.fromElements(new Tuple2<>("a", 1), + new Tuple2<>("a", 2)); +DataStreamSource> streamSource02 = env.fromElements(new Tuple2<>("b", 1), + new Tuple2<>("b", 2)); +streamSource01.union(streamSource02); +streamSource01.union(streamSource01,streamSource02); +``` + +### 2.7 Connect [DataStream,DataStream → ConnectedStreams] + +Connect 操作用于连接两个或者多个类型不同的 DataStream ,其返回的类型是 ConnectedStreams ,此时被连接的多个 DataStreams 可以共享彼此之间的数据状态。但是需要注意的是由于不同 DataStream 之间的数据类型是不同的,如果想要进行后续的计算操作,还需要通过 CoMap 或 CoFlatMap 将 ConnectedStreams 转换回 DataStream: + +```java +DataStreamSource> streamSource01 = env.fromElements(new Tuple2<>("a", 3), + new Tuple2<>("b", 5)); +DataStreamSource streamSource02 = env.fromElements(2, 3, 9); +// 使用connect进行连接 +ConnectedStreams, Integer> connect = streamSource01.connect(streamSource02); +connect.map(new CoMapFunction, Integer, Integer>() { + @Override + public Integer map1(Tuple2 value) throws Exception { + return value.f1; + } + + @Override + public Integer map2(Integer value) throws Exception { + return value; + } +}).map(x -> x * 100).print(); + +// 输出: +300 500 200 900 300 +``` + +### 2.8 Split 和 Select + +- **Split [DataStream → SplitStream]**:用于将一个 DataStream 按照指定规则进行拆分为多个 DataStream,需要注意的是这里进行的是逻辑拆分,即 Split 只是将数据贴上不同的类型标签,但最终返回的仍然只是一个 SplitStream; +- **Select [SplitStream → DataStream]**:想要从逻辑拆分的 SplitStream 中获取真实的不同类型的 DataStream,需要使用 Select 算子,示例如下: + +```java +DataStreamSource streamSource = env.fromElements(1, 2, 3, 4, 5, 6, 7, 8); +// 标记 +SplitStream split = streamSource.split(new OutputSelector() { + @Override + public Iterable select(Integer value) { + List output = new ArrayList(); + output.add(value % 2 == 0 ? "even" : "odd"); + return output; + } +}); +// 获取偶数数据集 +split.select("even").print(); +// 输出 2,4,6,8 +``` + +### 2.9 project [DataStream → DataStream] + +project 主要用于获取 tuples 中的指定字段集,示例如下: + +```java +DataStreamSource> streamSource = env.fromElements( + new Tuple3<>("li", 22, "2018-09-23"), + new Tuple3<>("ming", 33, "2020-09-23")); +streamSource.project(0,2).print(); + +// 输出 +(li,2018-09-23) +(ming,2020-09-23) +``` + +## 三、物理分区 + +物理分区 (Physical partitioning) 是 Flink 提供的底层的 API,允许用户采用内置的分区规则或者自定义的分区规则来对数据进行分区,从而避免数据在某些分区上过于倾斜,常用的分区规则如下: + +### 3.1 Random partitioning [DataStream → DataStream] + +随机分区 (Random partitioning) 用于随机的将数据分布到所有下游分区中,通过 shuffle 方法来进行实现: + +```java +dataStream.shuffle(); +``` + +### 3.2 Rebalancing [DataStream → DataStream] + +Rebalancing 采用轮询的方式将数据进行分区,其适合于存在数据倾斜的场景下,通过 rebalance 方法进行实现: + +```java +dataStream.rebalance(); +``` + +### 3.3 Rescaling [DataStream → DataStream] + +当采用 Rebalancing 进行分区平衡时,其实现的是全局性的负载均衡,数据会通过网络传输到其他节点上并完成分区数据的均衡。 而 Rescaling 则是低配版本的 rebalance,它不需要额外的网络开销,它只会对上下游的算子之间进行重新均衡,通过 rescale 方法进行实现: + +```java +dataStream.rescale(); +``` + +ReScale 这个单词具有重新缩放的意义,其对应的操作也是如此,具体如下:如果上游 operation 并行度为 2,而下游的 operation 并行度为 6,则其中 1 个上游的 operation 会将元素分发到 3 个下游 operation,另 1 个上游 operation 则会将元素分发到另外 3 个下游 operation。反之亦然,如果上游的 operation 并行度为 6,而下游 operation 并行度为 2,则其中 3 个上游 operation 会将元素分发到 1 个下游 operation,另 3 个上游 operation 会将元素分发到另外 1 个下游operation: + +
+ + +### 3.4 Broadcasting [DataStream → DataStream] + +将数据分发到所有分区上。通常用于小数据集与大数据集进行关联的情况下,此时可以将小数据集广播到所有分区上,避免频繁的跨分区关联,通过 broadcast 方法进行实现: + +```java +dataStream.broadcast(); +``` + +### 3.5 Custom partitioning [DataStream → DataStream] + +Flink 运行用户采用自定义的分区规则来实现分区,此时需要通过实现 Partitioner 接口来自定义分区规则,并指定对应的分区键,示例如下: + +```java + DataStreamSource> streamSource = env.fromElements(new Tuple2<>("Hadoop", 1), + new Tuple2<>("Spark", 1), + new Tuple2<>("Flink-streaming", 2), + new Tuple2<>("Flink-batch", 4), + new Tuple2<>("Storm", 4), + new Tuple2<>("HBase", 3)); +streamSource.partitionCustom(new Partitioner() { + @Override + public int partition(String key, int numPartitions) { + // 将第一个字段包含flink的Tuple2分配到同一个分区 + return key.toLowerCase().contains("flink") ? 0 : 1; + } +}, 0).print(); + + +// 输出如下: +1> (Flink-streaming,2) +1> (Flink-batch,4) +2> (Hadoop,1) +2> (Spark,1) +2> (Storm,4) +2> (HBase,3) +``` + + + +## 四、任务链和资源组 + +任务链和资源组 ( Task chaining and resource groups ) 也是 Flink 提供的底层 API,用于控制任务链和资源分配。默认情况下,如果操作允许 (例如相邻的两次 map 操作) ,则 Flink 会尝试将它们在同一个线程内进行,从而可以获取更好的性能。但是 Flink 也允许用户自己来控制这些行为,这就是任务链和资源组 API: + +### 4.1 startNewChain + +startNewChain 用于基于当前 operation 开启一个新的任务链。如下所示,基于第一个 map 开启一个新的任务链,此时前一个 map 和 后一个 map 将处于同一个新的任务链中,但它们与 filter 操作则分别处于不同的任务链中: + +```java +someStream.filter(...).map(...).startNewChain().map(...); +``` + +### 4.2 disableChaining + + disableChaining 操作用于禁止将其他操作与当前操作放置于同一个任务链中,示例如下: + +```java +someStream.map(...).disableChaining(); +``` + +### 4.3 slotSharingGroup + +slot 是任务管理器 (TaskManager) 所拥有资源的固定子集,每个操作 (operation) 的子任务 (sub task) 都需要获取 slot 来执行计算,但每个操作所需要资源的大小都是不相同的,为了更好地利用资源,Flink 允许不同操作的子任务被部署到同一 slot 中。slotSharingGroup 用于设置操作的 slot 共享组 (slot sharing group) ,Flink 会将具有相同 slot 共享组的操作放到同一个 slot 中 。示例如下: + +```java +someStream.filter(...).slotSharingGroup("slotSharingGroupName"); +``` + + + +## 参考资料 + +Flink Operators: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/ diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Windows.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Windows.md" new file mode 100644 index 0000000..4fb9cb8 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink_Windows.md" @@ -0,0 +1,128 @@ +# Flink Windows + + + + +## 一、窗口概念 + +在大多数场景下,我们需要统计的数据流都是无界的,因此我们无法等待整个数据流终止后才进行统计。通常情况下,我们只需要对某个时间范围或者数量范围内的数据进行统计分析:如每隔五分钟统计一次过去一小时内所有商品的点击量;或者每发生1000次点击后,都去统计一下每个商品点击率的占比。在 Flink 中,我们使用窗口 (Window) 来实现这类功能。按照统计维度的不同,Flink 中的窗口可以分为 时间窗口 (Time Windows) 和 计数窗口 (Count Windows) 。 + +## 二、Time Windows + +Time Windows 用于以时间为维度来进行数据聚合,具体分为以下四类: + +### 2.1 Tumbling Windows + +滚动窗口 (Tumbling Windows) 是指彼此之间没有重叠的窗口。例如:每隔1小时统计过去1小时内的商品点击量,那么 1 天就只能分为 24 个窗口,每个窗口彼此之间是不存在重叠的,具体如下: + +
+ + +这里我们以词频统计为例,给出一个具体的用例,代码如下: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +// 接收socket上的数据输入 +DataStreamSource streamSource = env.socketTextStream("hadoop001", 9999, "\n", 3); +streamSource.flatMap(new FlatMapFunction>() { + @Override + public void flatMap(String value, Collector> out) throws Exception { + String[] words = value.split("\t"); + for (String word : words) { + out.collect(new Tuple2<>(word, 1L)); + } + } +}).keyBy(0).timeWindow(Time.seconds(3)).sum(1).print(); //每隔3秒统计一次每个单词出现的数量 +env.execute("Flink Streaming"); +``` + +测试结果如下: + +
+ + + + +### 2.2 Sliding Windows + +滑动窗口用于滚动进行聚合分析,例如:每隔 6 分钟统计一次过去一小时内所有商品的点击量,那么统计窗口彼此之间就是存在重叠的,即 1天可以分为 240 个窗口。图示如下: + +
+ + +可以看到 window 1 - 4 这四个窗口彼此之间都存在着时间相等的重叠部分。想要实现滑动窗口,只需要在使用 timeWindow 方法时额外传递第二个参数作为滚动时间即可,具体如下: + +```java +// 每隔3秒统计一次过去1分钟内的数据 +timeWindow(Time.minutes(1),Time.seconds(3)) +``` + +### 2.3 Session Windows + +当用户在进行持续浏览时,可能每时每刻都会有点击数据,例如在活动区间内,用户可能频繁的将某类商品加入和移除购物车,而你只想知道用户本次浏览最终的购物车情况,此时就可以在用户持有的会话结束后再进行统计。想要实现这类统计,可以通过 Session Windows 来进行实现。 + +
+ + +具体的实现代码如下: + +```java +// 以处理时间为衡量标准,如果10秒内没有任何数据输入,就认为会话已经关闭,此时触发统计 +window(ProcessingTimeSessionWindows.withGap(Time.seconds(10))) +// 以事件时间为衡量标准 +window(EventTimeSessionWindows.withGap(Time.seconds(10))) +``` + +### 2.4 Global Windows + +最后一个窗口是全局窗口, 全局窗口会将所有 key 相同的元素分配到同一个窗口中,其通常配合触发器 (trigger) 进行使用。如果没有相应触发器,则计算将不会被执行。 + +
+ + +这里继续以上面词频统计的案例为例,示例代码如下: + +```java +// 当单词累计出现的次数每达到10次时,则触发计算,计算整个窗口内该单词出现的总数 +window(GlobalWindows.create()).trigger(CountTrigger.of(10)).sum(1).print(); +``` + +## 三、Count Windows + +Count Windows 用于以数量为维度来进行数据聚合,同样也分为滚动窗口和滑动窗口,实现方式也和时间窗口完全一致,只是调用的 API 不同,具体如下: + +```java +// 滚动计数窗口,每1000次点击则计算一次 +countWindow(1000) +// 滑动计数窗口,每10次点击发生后,则计算过去1000次点击的情况 +countWindow(1000,10) +``` + +实际上计数窗口内部就是调用的我们上一部分介绍的全局窗口来实现的,其源码如下: + +```java +public WindowedStream countWindow(long size) { + return window(GlobalWindows.create()).trigger(PurgingTrigger.of(CountTrigger.of(size))); +} + + +public WindowedStream countWindow(long size, long slide) { + return window(GlobalWindows.create()) + .evictor(CountEvictor.of(size)) + .trigger(CountTrigger.of(slide)); +} +``` + + + +## 参考资料 + +Flink Windows: https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/operators/windows.html diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..58185a9 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,304 @@ +# Flink 开发环境搭建 + + + + + +## 一、安装 Scala 插件 + +Flink 分别提供了基于 Java 语言和 Scala 语言的 API ,如果想要使用 Scala 语言来开发 Flink 程序,可以通过在 IDEA 中安装 Scala 插件来提供语法提示,代码高亮等功能。打开 IDEA , 依次点击 `File => settings => plugins` 打开插件安装页面,搜索 Scala 插件并进行安装,安装完成后,重启 IDEA 即可生效。 + +
+ +## 二、Flink 项目初始化 + +### 2.1 使用官方脚本构建 + +Flink 官方支持使用 Maven 和 Gradle 两种构建工具来构建基于 Java 语言的 Flink 项目;支持使用 SBT 和 Maven 两种构建工具来构建基于 Scala 语言的 Flink 项目。 这里以 Maven 为例进行说明,因为其可以同时支持 Java 语言和 Scala 语言项目的构建。需要注意的是 Flink 1.9 只支持 Maven 3.0.4 以上的版本,Maven 安装完成后,可以通过以下两种方式来构建项目: + +**1. 直接基于 Maven Archetype 构建** + +直接使用下面的 mvn 语句来进行构建,然后根据交互信息的提示,依次输入 groupId , artifactId 以及包名等信息后等待初始化的完成: + +```bash +$ mvn archetype:generate \ + -DarchetypeGroupId=org.apache.flink \ + -DarchetypeArtifactId=flink-quickstart-java \ + -DarchetypeVersion=1.9.0 +``` + +> 注:如果想要创建基于 Scala 语言的项目,只需要将 flink-quickstart-java 换成 flink-quickstart-scala 即可,后文亦同。 + +**2. 使用官方脚本快速构建** + +为了更方便的初始化项目,官方提供了快速构建脚本,可以直接通过以下命令来进行调用: + +```shell +$ curl https://flink.apache.org/q/quickstart.sh | bash -s 1.9.0 +``` + +该方式其实也是通过执行 maven archetype 命令来进行初始化,其脚本内容如下: + +```shell +PACKAGE=quickstart + +mvn archetype:generate \ + -DarchetypeGroupId=org.apache.flink \ + -DarchetypeArtifactId=flink-quickstart-java \ + -DarchetypeVersion=${1:-1.8.0} \ + -DgroupId=org.myorg.quickstart \ + -DartifactId=$PACKAGE \ + -Dversion=0.1 \ + -Dpackage=org.myorg.quickstart \ + -DinteractiveMode=false +``` + +可以看到相比于第一种方式,该种方式只是直接指定好了 groupId ,artifactId ,version 等信息而已。 + +### 2.2 使用 IDEA 构建 + +如果你使用的是开发工具是 IDEA ,可以直接在项目创建页面选择 Maven Flink Archetype 进行项目初始化: + +
+ +如果你的 IDEA 没有上述 Archetype, 可以通过点击右上角的 `ADD ARCHETYPE` ,来进行添加,依次填入所需信息,这些信息都可以从上述的 `archetype:generate ` 语句中获取。点击 `OK` 保存后,该 Archetype 就会一直存在于你的 IDEA 中,之后每次创建项目时,只需要直接选择该 Archetype 即可: + +
+ +选中 Flink Archetype ,然后点击 `NEXT` 按钮,之后的所有步骤都和正常的 Maven 工程相同。 + +## 三、项目结构 + +### 3.1 项目结构 + +创建完成后的自动生成的项目结构如下: + +
+ +其中 BatchJob 为批处理的样例代码,源码如下: + +```scala +import org.apache.flink.api.scala._ + +object BatchJob { + def main(args: Array[String]) { + val env = ExecutionEnvironment.getExecutionEnvironment + .... + env.execute("Flink Batch Scala API Skeleton") + } +} +``` + +getExecutionEnvironment 代表获取批处理的执行环境,如果是本地运行则获取到的就是本地的执行环境;如果在集群上运行,得到的就是集群的执行环境。如果想要获取流处理的执行环境,则只需要将 `ExecutionEnvironment` 替换为 `StreamExecutionEnvironment`, 对应的代码样例在 StreamingJob 中: + +```scala +import org.apache.flink.streaming.api.scala._ + +object StreamingJob { + def main(args: Array[String]) { + val env = StreamExecutionEnvironment.getExecutionEnvironment + ... + env.execute("Flink Streaming Scala API Skeleton") + } +} + +``` + +需要注意的是对于流处理项目 `env.execute()` 这句代码是必须的,否则流处理程序就不会被执行,但是对于批处理项目则是可选的。 + +### 3.2 主要依赖 + +基于 Maven 骨架创建的项目主要提供了以下核心依赖:其中 `flink-scala` 用于支持开发批处理程序 ;`flink-streaming-scala` 用于支持开发流处理程序 ;`scala-library` 用于提供 Scala 语言所需要的类库。如果在使用 Maven 骨架创建时选择的是 Java 语言,则默认提供的则是 `flink-java` 和 `flink-streaming-java` 依赖。 + +```xml + + + + org.apache.flink + flink-scala_${scala.binary.version} + ${flink.version} + provided + + + org.apache.flink + flink-streaming-scala_${scala.binary.version} + ${flink.version} + provided + + + + + org.scala-lang + scala-library + ${scala.version} + provided + +``` + +需要特别注意的以上依赖的 `scope` 标签全部被标识为 provided ,这意味着这些依赖都不会被打入最终的 JAR 包。因为 Flink 的安装包中已经提供了这些依赖,位于其 lib 目录下,名为 `flink-dist_*.jar` ,它包含了 Flink 的所有核心类和依赖: + +
+ + `scope` 标签被标识为 provided 会导致你在 IDEA 中启动项目时会抛出 ClassNotFoundException 异常。基于这个原因,在使用 IDEA 创建项目时还自动生成了以下 profile 配置: + +```xml + + + + + + add-dependencies-for-IDEA + + + + idea.version + + + + + + org.apache.flink + flink-scala_${scala.binary.version} + ${flink.version} + compile + + + org.apache.flink + flink-streaming-scala_${scala.binary.version} + ${flink.version} + compile + + + org.scala-lang + scala-library + ${scala.version} + compile + + + + +``` + +在 id 为 `add-dependencies-for-IDEA` 的 profile 中,所有的核心依赖都被标识为 compile,此时你可以无需改动任何代码,只需要在 IDEA 的 Maven 面板中勾选该 profile,即可直接在 IDEA 中运行 Flink 项目: + +
+ +## 四、词频统计案例 + +项目创建完成后,可以先书写一个简单的词频统计的案例来尝试运行 Flink 项目,以下以 Scala 语言为例,分别介绍流处理程序和批处理程序的编程示例: + +### 4.1 批处理示例 + +```scala +import org.apache.flink.api.scala._ + +object WordCountBatch { + + def main(args: Array[String]): Unit = { + val benv = ExecutionEnvironment.getExecutionEnvironment + val dataSet = benv.readTextFile("D:\\wordcount.txt") + dataSet.flatMap { _.toLowerCase.split(",")} + .filter (_.nonEmpty) + .map { (_, 1) } + .groupBy(0) + .sum(1) + .print() + } +} +``` + +其中 `wordcount.txt` 中的内容如下: + +```shell +a,a,a,a,a +b,b,b +c,c +d,d +``` + +本机不需要配置其他任何的 Flink 环境,直接运行 Main 方法即可,结果如下: + +
+ +### 4.2 流处理示例 + +```scala +import org.apache.flink.streaming.api.scala._ +import org.apache.flink.streaming.api.windowing.time.Time + +object WordCountStreaming { + + def main(args: Array[String]): Unit = { + + val senv = StreamExecutionEnvironment.getExecutionEnvironment + + val dataStream: DataStream[String] = senv.socketTextStream("192.168.0.229", 9999, '\n') + dataStream.flatMap { line => line.toLowerCase.split(",") } + .filter(_.nonEmpty) + .map { word => (word, 1) } + .keyBy(0) + .timeWindow(Time.seconds(3)) + .sum(1) + .print() + senv.execute("Streaming WordCount") + } +} +``` + +这里以监听指定端口号上的内容为例,使用以下命令来开启端口服务: + +```shell +nc -lk 9999 +``` + +之后输入测试数据即可观察到流处理程序的处理情况。 + +## 五、使用 Scala Shell + +对于日常的 Demo 项目,如果你不想频繁地启动 IDEA 来观察测试结果,可以像 Spark 一样,直接使用 Scala Shell 来运行程序,这对于日常的学习来说,效果更加直观,也更省时。Flink 安装包的下载地址如下: + +```shell +https://flink.apache.org/downloads.html +``` + +Flink 大多数版本都提供有 Scala 2.11 和 Scala 2.12 两个版本的安装包可供下载: + +
+ +下载完成后进行解压即可,Scala Shell 位于安装目录的 bin 目录下,直接使用以下命令即可以本地模式启动: + +```shell +./start-scala-shell.sh local +``` + +命令行启动完成后,其已经提供了批处理 (benv 和 btenv)和流处理(senv 和 stenv)的运行环境,可以直接运行 Scala Flink 程序,示例如下: + +
+ +最后解释一个常见的异常:这里我使用的 Flink 版本为 1.9.1,启动时会抛出如下异常。这里因为按照官方的说明,目前所有 Scala 2.12 版本的安装包暂时都不支持 Scala Shell,所以如果想要使用 Scala Shell,只能选择 Scala 2.11 版本的安装包。 + +```shell +[root@hadoop001 bin]# ./start-scala-shell.sh local +错误: 找不到或无法加载主类 org.apache.flink.api.scala.FlinkShell +``` + + + + + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\346\240\270\345\277\203\346\246\202\345\277\265\347\273\274\350\277\260.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\346\240\270\345\277\203\346\246\202\345\277\265\347\273\274\350\277\260.md" new file mode 100644 index 0000000..dd94a54 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\346\240\270\345\277\203\346\246\202\345\277\265\347\273\274\350\277\260.md" @@ -0,0 +1,173 @@ +# Flink 核心概念综述 + + + +## 一、Flink 简介 + +Apache Flink 诞生于柏林工业大学的一个研究性项目,原名 StratoSphere 。2014 年,由 StratoSphere 项目孵化出 Flink,并于同年捐赠 Apache,之后成为 Apache 的顶级项目。2019 年 1 年,阿里巴巴收购了 Flink 的母公司 Data Artisans,并宣布开源内部的 Blink,Blink 是阿里巴巴基于 Flink 优化后的版本,增加了大量的新功能,并在性能和稳定性上进行了各种优化,经历过阿里内部多种复杂业务的挑战和检验。同时阿里巴巴也表示会逐步将这些新功能和特性 Merge 回社区版本的 Flink 中,因此 Flink 成为目前最为火热的大数据处理框架。 + +简单来说,Flink 是一个分布式的流处理框架,它能够对有界和无界的数据流进行高效的处理。Flink 的核心是流处理,当然它也能支持批处理,Flink 将批处理看成是流处理的一种特殊情况,即数据流是有明确界限的。这和 Spark Streaming 的思想是完全相反的,Spark Streaming 的核心是批处理,它将流处理看成是批处理的一种特殊情况, 即把数据流进行极小粒度的拆分,拆分为多个微批处理。 + +Flink 有界数据流和无界数据流: + +
+ + + + +Spark Streaming 数据流的拆分: + +
+ + + + +## 二、Flink 核心架构 + +Flink 采用分层的架构设计,从而保证各层在功能和职责上的清晰。如下图所示,由上而下分别是 API & Libraries 层、Runtime 核心层以及物理部署层: + +
+ + + + +### 2.1 API & Libraries 层 + +这一层主要提供了编程 API 和 顶层类库: + ++ 编程 API : 用于进行流处理的 DataStream API 和用于进行批处理的 DataSet API; ++ 顶层类库:包括用于复杂事件处理的 CEP 库;用于结构化数据查询的 SQL & Table 库,以及基于批处理的机器学习库 FlinkML 和 图形处理库 Gelly。 + +### 2.2 Runtime 核心层 + +这一层是 Flink 分布式计算框架的核心实现层,包括作业转换,任务调度,资源分配,任务执行等功能,基于这一层的实现,可以在流式引擎下同时运行流处理程序和批处理程序。 + +### 2.3 物理部署层 + +Flink 的物理部署层,用于支持在不同平台上部署运行 Flink 应用。 + +## 三、Flink 分层 API + +在上面介绍的 API & Libraries 这一层,Flink 又进行了更为具体的划分。具体如下: + +
+ + + + +按照如上的层次结构,API 的一致性由下至上依次递增,接口的表现能力由下至上依次递减,各层的核心功能如下: + +### 3.1 SQL & Table API + +SQL & Table API 同时适用于批处理和流处理,这意味着你可以对有界数据流和无界数据流以相同的语义进行查询,并产生相同的结果。除了基本查询外, 它还支持自定义的标量函数,聚合函数以及表值函数,可以满足多样化的查询需求。 + +### 3.2 DataStream & DataSet API + +DataStream & DataSet API 是 Flink 数据处理的核心 API,支持使用 Java 语言或 Scala 语言进行调用,提供了数据读取,数据转换和数据输出等一系列常用操作的封装。 + +### 3.3 Stateful Stream Processing + +Stateful Stream Processing 是最低级别的抽象,它通过 Process Function 函数内嵌到 DataStream API 中。 Process Function 是 Flink 提供的最底层 API,具有最大的灵活性,允许开发者对于时间和状态进行细粒度的控制。 + +## 四、Flink 集群架构 + +### 4.1 核心组件 + +按照上面的介绍,Flink 核心架构的第二层是 Runtime 层, 该层采用标准的 Master - Slave 结构, 其中,Master 部分又包含了三个核心组件:Dispatcher、ResourceManager 和 JobManager,而 Slave 则主要是 TaskManager 进程。它们的功能分别如下: + +- **JobManagers** (也称为 *masters*) :JobManagers 接收由 Dispatcher 传递过来的执行程序,该执行程序包含了作业图 (JobGraph),逻辑数据流图 (logical dataflow graph) 及其所有的 classes 文件以及第三方类库 (libraries) 等等 。紧接着 JobManagers 会将 JobGraph 转换为执行图 (ExecutionGraph),然后向 ResourceManager 申请资源来执行该任务,一旦申请到资源,就将执行图分发给对应的 TaskManagers 。因此每个作业 (Job) 至少有一个 JobManager;高可用部署下可以有多个 JobManagers,其中一个作为 *leader*,其余的则处于 *standby* 状态。 +- **TaskManagers** (也称为 *workers*) : TaskManagers 负责实际的子任务 (subtasks) 的执行,每个 TaskManagers 都拥有一定数量的 slots。Slot 是一组固定大小的资源的合集 (如计算能力,存储空间)。TaskManagers 启动后,会将其所拥有的 slots 注册到 ResourceManager 上,由 ResourceManager 进行统一管理。 +- **Dispatcher**:负责接收客户端提交的执行程序,并传递给 JobManager 。除此之外,它还提供了一个 WEB UI 界面,用于监控作业的执行情况。 +- **ResourceManager** :负责管理 slots 并协调集群资源。ResourceManager 接收来自 JobManager 的资源请求,并将存在空闲 slots 的 TaskManagers 分配给 JobManager 执行任务。Flink 基于不同的部署平台,如 YARN , Mesos,K8s 等提供了不同的资源管理器,当 TaskManagers 没有足够的 slots 来执行任务时,它会向第三方平台发起会话来请求额外的资源。 + +
+ + +### 4.2 Task & SubTask + +上面我们提到:TaskManagers 实际执行的是 SubTask,而不是 Task,这里解释一下两者的区别: + +在执行分布式计算时,Flink 将可以链接的操作 (operators) 链接到一起,这就是 Task。之所以这样做, 是为了减少线程间切换和缓冲而导致的开销,在降低延迟的同时可以提高整体的吞吐量。 但不是所有的 operator 都可以被链接,如下 keyBy 等操作会导致网络 shuffle 和重分区,因此其就不能被链接,只能被单独作为一个 Task。 简单来说,一个 Task 就是一个可以链接的最小的操作链 (Operator Chains) 。如下图,source 和 map 算子被链接到一块,因此整个作业就只有三个 Task: + +
+ + +解释完 Task ,我们在解释一下什么是 SubTask,其准确的翻译是: *A subtask is one parallel slice of a task*,即一个 Task 可以按照其并行度拆分为多个 SubTask。如上图,source & map 具有两个并行度,KeyBy 具有两个并行度,Sink 具有一个并行度,因此整个虽然只有 3 个 Task,但是却有 5 个 SubTask。Jobmanager 负责定义和拆分这些 SubTask,并将其交给 Taskmanagers 来执行,每个 SubTask 都是一个单独的线程。 + +### 4.3 资源管理 + +理解了 SubTasks ,我们再来看看其与 Slots 的对应情况。一种可能的分配情况如下: + +
+ + + + +这时每个 SubTask 线程运行在一个独立的 TaskSlot, 它们共享所属的 TaskManager 进程的TCP 连接(通过多路复用技术)和心跳信息 (heartbeat messages),从而可以降低整体的性能开销。此时看似是最好的情况,但是每个操作需要的资源都是不尽相同的,这里假设该作业 keyBy 操作所需资源的数量比 Sink 多很多 ,那么此时 Sink 所在 Slot 的资源就没有得到有效的利用。 + +基于这个原因,Flink 允许多个 subtasks 共享 slots,即使它们是不同 tasks 的 subtasks,但只要它们来自同一个 Job 就可以。假设上面 souce & map 和 keyBy 的并行度调整为 6,而 Slot 的数量不变,此时情况如下: + +
+ + + + +可以看到一个 Task Slot 中运行了多个 SubTask 子任务,此时每个子任务仍然在一个独立的线程中执行,只不过共享一组 Sot 资源而已。那么 Flink 到底如何确定一个 Job 至少需要多少个 Slot 呢?Flink 对于这个问题的处理很简单,默认情况一个 Job 所需要的 Slot 的数量就等于其 Operation 操作的最高并行度。如下, A,B,D 操作的并行度为 4,而 C,E 操作的并行度为 2,那么此时整个 Job 就需要至少四个 Slots 来完成。通过这个机制,Flink 就可以不必去关心一个 Job 到底会被拆分为多少个 Tasks 和 SubTasks。 + +
+ + + + + + +### 4.4 组件通讯 + +Flink 的所有组件都基于 Actor System 来进行通讯。Actor system是多种角色的 actor 的容器,它提供调度,配置,日志记录等多种服务,并包含一个可以启动所有 actor 的线程池,如果 actor 是本地的,则消息通过共享内存进行共享,但如果 actor 是远程的,则通过 RPC 的调用来传递消息。 + +
+ + + + +## 五、Flink 的优点 + +最后基于上面的介绍,来总结一下 Flink 的优点: + ++ Flink 是基于事件驱动 (Event-driven) 的应用,能够同时支持流处理和批处理; ++ 基于内存的计算,能够保证高吞吐和低延迟,具有优越的性能表现; ++ 支持精确一次 (Exactly-once) 语意,能够完美地保证一致性和正确性; ++ 分层 API ,能够满足各个层次的开发需求; ++ 支持高可用配置,支持保存点机制,能够提供安全性和稳定性上的保证; ++ 多样化的部署方式,支持本地,远端,云端等多种部署方案; ++ 具有横向扩展架构,能够按照用户的需求进行动态扩容; ++ 活跃度极高的社区和完善的生态圈的支持。 + + + +## 参考资料 + ++ [Dataflow Programming Model](https://ci.apache.org/projects/flink/flink-docs-release-1.9/concepts/programming-model.html) ++ [Distributed Runtime Environment](https://ci.apache.org/projects/flink/flink-docs-release-1.9/concepts/runtime.html) ++ [Component Stack](https://ci.apache.org/projects/flink/flink-docs-release-1.9/internals/components.html) ++ Fabian Hueske , Vasiliki Kalavri . 《Stream Processing with Apache Flink》. O'Reilly Media . 2019-4-30 + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\347\212\266\346\200\201\347\256\241\347\220\206\344\270\216\346\243\200\346\237\245\347\202\271\346\234\272\345\210\266.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\347\212\266\346\200\201\347\256\241\347\220\206\344\270\216\346\243\200\346\237\245\347\202\271\346\234\272\345\210\266.md" new file mode 100644 index 0000000..e19d9d5 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flink\347\212\266\346\200\201\347\256\241\347\220\206\344\270\216\346\243\200\346\237\245\347\202\271\346\234\272\345\210\266.md" @@ -0,0 +1,370 @@ +# Flink 状态管理 + + + +## 一、状态分类 + +相对于其他流计算框架,Flink 一个比较重要的特性就是其支持有状态计算。即你可以将中间的计算结果进行保存,并提供给后续的计算使用: + +
+ + + +具体而言,Flink 又将状态 (State) 分为 Keyed State 与 Operator State: + +### 2.1 算子状态 + +算子状态 (Operator State):顾名思义,状态是和算子进行绑定的,一个算子的状态不能被其他算子所访问到。官方文档上对 Operator State 的解释是:*each operator state is bound to one parallel operator instance*,所以更为确切的说一个算子状态是与一个并发的算子实例所绑定的,即假设算子的并行度是 2,那么其应有两个对应的算子状态: + +
+ + + +### 2.2 键控状态 + +键控状态 (Keyed State) :是一种特殊的算子状态,即状态是根据 key 值进行区分的,Flink 会为每类键值维护一个状态实例。如下图所示,每个颜色代表不同 key 值,对应四个不同的状态实例。需要注意的是键控状态只能在 `KeyedStream` 上进行使用,我们可以通过 `stream.keyBy(...)` 来得到 `KeyedStream` 。 + +
+ + + +## 二、状态编程 + +### 2.1 键控状态 + +Flink 提供了以下数据格式来管理和存储键控状态 (Keyed State): + +- **ValueState**:存储单值类型的状态。可以使用 `update(T)` 进行更新,并通过 `T value()` 进行检索。 +- **ListState**:存储列表类型的状态。可以使用 `add(T)` 或 `addAll(List)` 添加元素;并通过 `get()` 获得整个列表。 +- **ReducingState**:用于存储经过 ReduceFunction 计算后的结果,使用 `add(T)` 增加元素。 +- **AggregatingState**:用于存储经过 AggregatingState 计算后的结果,使用 `add(IN)` 添加元素。 +- **FoldingState**:已被标识为废弃,会在未来版本中移除,官方推荐使用 `AggregatingState` 代替。 +- **MapState**:维护 Map 类型的状态。 + +以上所有增删改查方法不必硬记,在使用时通过语法提示来调用即可。这里给出一个具体的使用示例:假设我们正在开发一个监控系统,当监控数据超过阈值一定次数后,需要发出报警信息。这里之所以要达到一定次数,是因为由于偶发原因,偶尔一次超过阈值并不能代表什么,故需要达到一定次数后才触发报警,这就需要使用到 Flink 的状态编程。相关代码如下: + +```java +public class ThresholdWarning extends + RichFlatMapFunction, Tuple2>> { + + // 通过ListState来存储非正常数据的状态 + private transient ListState abnormalData; + // 需要监控的阈值 + private Long threshold; + // 触发报警的次数 + private Integer numberOfTimes; + + ThresholdWarning(Long threshold, Integer numberOfTimes) { + this.threshold = threshold; + this.numberOfTimes = numberOfTimes; + } + + @Override + public void open(Configuration parameters) { + // 通过状态名称(句柄)获取状态实例,如果不存在则会自动创建 + abnormalData = getRuntimeContext().getListState( + new ListStateDescriptor<>("abnormalData", Long.class)); + } + + @Override + public void flatMap(Tuple2 value, Collector>> out) + throws Exception { + Long inputValue = value.f1; + // 如果输入值超过阈值,则记录该次不正常的数据信息 + if (inputValue >= threshold) { + abnormalData.add(inputValue); + } + ArrayList list = Lists.newArrayList(abnormalData.get().iterator()); + // 如果不正常的数据出现达到一定次数,则输出报警信息 + if (list.size() >= numberOfTimes) { + out.collect(Tuple2.of(value.f0 + " 超过指定阈值 ", list)); + // 报警信息输出后,清空状态 + abnormalData.clear(); + } + } +} +``` + +调用自定义的状态监控,这里我们使用 a,b 来代表不同类型的监控数据,分别对其数据进行监控: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +DataStreamSource> tuple2DataStreamSource = env.fromElements( + Tuple2.of("a", 50L), Tuple2.of("a", 80L), Tuple2.of("a", 400L), + Tuple2.of("a", 100L), Tuple2.of("a", 200L), Tuple2.of("a", 200L), + Tuple2.of("b", 100L), Tuple2.of("b", 200L), Tuple2.of("b", 200L), + Tuple2.of("b", 500L), Tuple2.of("b", 600L), Tuple2.of("b", 700L)); +tuple2DataStreamSource + .keyBy(0) + .flatMap(new ThresholdWarning(100L, 3)) // 超过100的阈值3次后就进行报警 + .printToErr(); +env.execute("Managed Keyed State"); +``` + +输出如下结果如下: + +
+ + + +### 2.2 状态有效期 + +以上任何类型的 keyed state 都支持配置有效期 (TTL) ,示例如下: + +```java +StateTtlConfig ttlConfig = StateTtlConfig + // 设置有效期为 10 秒 + .newBuilder(Time.seconds(10)) + // 设置有效期更新规则,这里设置为当创建和写入时,都重置其有效期到规定的10秒 + .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) + /*设置只要值过期就不可见,另外一个可选值是ReturnExpiredIfNotCleanedUp, + 代表即使值过期了,但如果还没有被物理删除,就是可见的*/ + .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired) + .build(); +ListStateDescriptor descriptor = new ListStateDescriptor<>("abnormalData", Long.class); +descriptor.enableTimeToLive(ttlConfig); +``` + +### 2.3 算子状态 + +相比于键控状态,算子状态目前支持的存储类型只有以下三种: + +- **ListState**:存储列表类型的状态。 +- **UnionListState**:存储列表类型的状态,与 ListState 的区别在于:如果并行度发生变化,ListState 会将该算子的所有并发的状态实例进行汇总,然后均分给新的 Task;而 UnionListState 只是将所有并发的状态实例汇总起来,具体的划分行为则由用户进行定义。 +- **BroadcastState**:用于广播的算子状态。 + +这里我们继续沿用上面的例子,假设此时我们不需要区分监控数据的类型,只要有监控数据超过阈值并达到指定的次数后,就进行报警,代码如下: + +```java +public class ThresholdWarning extends RichFlatMapFunction, +Tuple2>>> implements CheckpointedFunction { + + // 非正常数据 + private List> bufferedData; + // checkPointedState + private transient ListState> checkPointedState; + // 需要监控的阈值 + private Long threshold; + // 次数 + private Integer numberOfTimes; + + ThresholdWarning(Long threshold, Integer numberOfTimes) { + this.threshold = threshold; + this.numberOfTimes = numberOfTimes; + this.bufferedData = new ArrayList<>(); + } + + @Override + public void initializeState(FunctionInitializationContext context) throws Exception { + // 注意这里获取的是OperatorStateStore + checkPointedState = context.getOperatorStateStore(). + getListState(new ListStateDescriptor<>("abnormalData", + TypeInformation.of(new TypeHint>() { + }))); + // 如果发生重启,则需要从快照中将状态进行恢复 + if (context.isRestored()) { + for (Tuple2 element : checkPointedState.get()) { + bufferedData.add(element); + } + } + } + + @Override + public void flatMap(Tuple2 value, + Collector>>> out) { + Long inputValue = value.f1; + // 超过阈值则进行记录 + if (inputValue >= threshold) { + bufferedData.add(value); + } + // 超过指定次数则输出报警信息 + if (bufferedData.size() >= numberOfTimes) { + // 顺便输出状态实例的hashcode + out.collect(Tuple2.of(checkPointedState.hashCode() + "阈值警报!", bufferedData)); + bufferedData.clear(); + } + } + + @Override + public void snapshotState(FunctionSnapshotContext context) throws Exception { + // 在进行快照时,将数据存储到checkPointedState + checkPointedState.clear(); + for (Tuple2 element : bufferedData) { + checkPointedState.add(element); + } + } +} +``` + +调用自定义算子状态,这里需要将并行度设置为 1: + +```java +final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); +// 开启检查点机制 +env.enableCheckpointing(1000); +// 设置并行度为1 +DataStreamSource> tuple2DataStreamSource = env.setParallelism(1).fromElements( + Tuple2.of("a", 50L), Tuple2.of("a", 80L), Tuple2.of("a", 400L), + Tuple2.of("a", 100L), Tuple2.of("a", 200L), Tuple2.of("a", 200L), + Tuple2.of("b", 100L), Tuple2.of("b", 200L), Tuple2.of("b", 200L), + Tuple2.of("b", 500L), Tuple2.of("b", 600L), Tuple2.of("b", 700L)); +tuple2DataStreamSource + .flatMap(new ThresholdWarning(100L, 3)) + .printToErr(); +env.execute("Managed Keyed State"); +} +``` + +此时输出如下: + +
+ + + +在上面的调用代码中,我们将程序的并行度设置为 1,可以看到三次输出中状态实例的 hashcode 全是一致的,证明它们都同一个状态实例。假设将并行度设置为 2,此时输出如下: + +
+ + + +可以看到此时两次输出中状态实例的 hashcode 是不一致的,代表它们不是同一个状态实例,这也就是上文提到的,一个算子状态是与一个并发的算子实例所绑定的。同时这里只输出两次,是因为在并发处理的情况下,线程 1 可能拿到 5 个非正常值,线程 2 可能拿到 4 个非正常值,因为要大于 3 次才能输出,所以在这种情况下就会出现只输出两条记录的情况,所以需要将程序的并行度设置为 1。 + +## 三、检查点机制 + +### 3.1 CheckPoints + +为了使 Flink 的状态具有良好的容错性,Flink 提供了检查点机制 (CheckPoints) 。通过检查点机制,Flink 定期在数据流上生成 checkpoint barrier ,当某个算子收到 barrier 时,即会基于当前状态生成一份快照,然后再将该 barrier 传递到下游算子,下游算子接收到该 barrier 后,也基于当前状态生成一份快照,依次传递直至到最后的 Sink 算子上。当出现异常后,Flink 就可以根据最近的一次的快照数据将所有算子恢复到先前的状态。 + +
+ + + + + +### 3.2 开启检查点 + +默认情况下,检查点机制是关闭的,需要在程序中进行开启: + +```java +// 开启检查点机制,并指定状态检查点之间的时间间隔 +env.enableCheckpointing(1000); + +// 其他可选配置如下: +// 设置语义 +env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE); +// 设置两个检查点之间的最小时间间隔 +env.getCheckpointConfig().setMinPauseBetweenCheckpoints(500); +// 设置执行Checkpoint操作时的超时时间 +env.getCheckpointConfig().setCheckpointTimeout(60000); +// 设置最大并发执行的检查点的数量 +env.getCheckpointConfig().setMaxConcurrentCheckpoints(1); +// 将检查点持久化到外部存储 +env.getCheckpointConfig().enableExternalizedCheckpoints( + ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION); +// 如果有更近的保存点时,是否将作业回退到该检查点 +env.getCheckpointConfig().setPreferCheckpointForRecovery(true); +``` + +### 3.3 保存点机制 + +保存点机制 (Savepoints) 是检查点机制的一种特殊的实现,它允许你通过手工的方式来触发 Checkpoint,并将结果持久化存储到指定路径中,主要用于避免 Flink 集群在重启或升级时导致状态丢失。示例如下: + +```shell +# 触发指定id的作业的Savepoint,并将结果存储到指定目录下 +bin/flink savepoint :jobId [:targetDirectory] +``` + +更多命令和配置可以参考官方文档:[savepoints]( https://ci.apache.org/projects/flink/flink-docs-release-1.9/zh/ops/state/savepoints.html ) + +## 四、状态后端 + +### 4.1 状态管理器分类 + +默认情况下,所有的状态都存储在 JVM 的堆内存中,在状态数据过多的情况下,这种方式很有可能导致内存溢出,因此 Flink 该提供了其它方式来存储状态数据,这些存储方式统一称为状态后端 (或状态管理器): + +
+ + + +主要有以下三种: + +#### 1. MemoryStateBackend + +默认的方式,即基于 JVM 的堆内存进行存储,主要适用于本地开发和调试。 + +#### 2. FsStateBackend + +基于文件系统进行存储,可以是本地文件系统,也可以是 HDFS 等分布式文件系统。 需要注意而是虽然选择使用了 FsStateBackend ,但正在进行的数据仍然是存储在 TaskManager 的内存中的,只有在 checkpoint 时,才会将状态快照写入到指定文件系统上。 + +#### 3. RocksDBStateBackend + +RocksDBStateBackend 是 Flink 内置的第三方状态管理器,采用嵌入式的 key-value 型数据库 RocksDB 来存储正在进行的数据。等到 checkpoint 时,再将其中的数据持久化到指定的文件系统中,所以采用 RocksDBStateBackend 时也需要配置持久化存储的文件系统。之所以这样做是因为 RocksDB 作为嵌入式数据库安全性比较低,但比起全文件系统的方式,其读取速率更快;比起全内存的方式,其存储空间更大,因此它是一种比较均衡的方案。 + +### 4.2 配置方式 + +Flink 支持使用两种方式来配置后端管理器: + +**第一种方式**:基于代码方式进行配置,只对当前作业生效: + +```java +// 配置 FsStateBackend +env.setStateBackend(new FsStateBackend("hdfs://namenode:40010/flink/checkpoints")); +// 配置 RocksDBStateBackend +env.setStateBackend(new RocksDBStateBackend("hdfs://namenode:40010/flink/checkpoints")); +``` + +配置 RocksDBStateBackend 时,需要额外导入下面的依赖: + +```xml + + org.apache.flink + flink-statebackend-rocksdb_2.11 + 1.9.0 + +``` + +**第二种方式**:基于 `flink-conf.yaml` 配置文件的方式进行配置,对所有部署在该集群上的作业都生效: + +```yaml +state.backend: filesystem +state.checkpoints.dir: hdfs://namenode:40010/flink/checkpoints +``` + + + +> 注:本篇文章所有示例代码下载地址:[flink-state-management]( https://github.com/heibaiying/BigData-Notes/tree/master/code/Flink/flink-state-management) + + + +## 参考资料 + ++ [Working with State](https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/state/state.html) ++ [Checkpointing](https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/stream/state/checkpointing.html) ++ [Savepoints](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/state/savepoints.html#savepoints) ++ [State Backends](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/state/state_backends.html) ++ Fabian Hueske , Vasiliki Kalavri . 《Stream Processing with Apache Flink》. O'Reilly Media . 2019-4-30 + + + + + + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\346\225\264\345\220\210Kafka.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\346\225\264\345\220\210Kafka.md" new file mode 100644 index 0000000..63e244b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\346\225\264\345\220\210Kafka.md" @@ -0,0 +1,116 @@ +# Flume 整合 Kafka + + + +## 一、背景 + +先说一下,为什么要使用 Flume + Kafka? + +以实时流处理项目为例,由于采集的数据量可能存在峰值和峰谷,假设是一个电商项目,那么峰值通常出现在秒杀时,这时如果直接将 Flume 聚合后的数据输入到 Storm 等分布式计算框架中,可能就会超过集群的处理能力,这时采用 Kafka 就可以起到削峰的作用。Kafka 天生为大数据场景而设计,具有高吞吐的特性,能很好地抗住峰值数据的冲击。 + +
+ + + +## 二、整合流程 + +Flume 发送数据到 Kafka 上主要是通过 `KafkaSink` 来实现的,主要步骤如下: + +### 1. 启动Zookeeper和Kafka + +这里启动一个单节点的 Kafka 作为测试: + +```shell +# 启动Zookeeper +zkServer.sh start + +# 启动kafka +bin/kafka-server-start.sh config/server.properties +``` + +### 2. 创建主题 + +创建一个主题 `flume-kafka`,之后 Flume 收集到的数据都会发到这个主题上: + +```shell +# 创建主题 +bin/kafka-topics.sh --create \ +--zookeeper hadoop001:2181 \ +--replication-factor 1 \ +--partitions 1 --topic flume-kafka + +# 查看创建的主题 +bin/kafka-topics.sh --zookeeper hadoop001:2181 --list +``` + + + +### 3. 启动kafka消费者 + +启动一个消费者,监听我们刚才创建的 `flume-kafka` 主题: + +```shell +# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic flume-kafka +``` + + + +### 4. 配置Flume + +新建配置文件 `exec-memory-kafka.properties`,文件内容如下。这里我们监听一个名为 `kafka.log` 的文件,当文件内容有变化时,将新增加的内容发送到 Kafka 的 `flume-kafka` 主题上。 + +```properties +a1.sources = s1 +a1.channels = c1 +a1.sinks = k1 + +a1.sources.s1.type=exec +a1.sources.s1.command=tail -F /tmp/kafka.log +a1.sources.s1.channels=c1 + +#设置Kafka接收器 +a1.sinks.k1.type= org.apache.flume.sink.kafka.KafkaSink +#设置Kafka地址 +a1.sinks.k1.brokerList=hadoop001:9092 +#设置发送到Kafka上的主题 +a1.sinks.k1.topic=flume-kafka +#设置序列化方式 +a1.sinks.k1.serializer.class=kafka.serializer.StringEncoder +a1.sinks.k1.channel=c1 + +a1.channels.c1.type=memory +a1.channels.c1.capacity=10000 +a1.channels.c1.transactionCapacity=100 +``` + + + +### 5. 启动Flume + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/exec-memory-kafka.properties \ +--name a1 -Dflume.root.logger=INFO,console +``` + + + +### 6. 测试 + +向监听的 `/tmp/kafka.log ` 文件中追加内容,查看 Kafka 消费者的输出: + +
+ +可以看到 `flume-kafka` 主题的消费端已经收到了对应的消息: + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\347\256\200\344\273\213\345\217\212\345\237\272\346\234\254\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\347\256\200\344\273\213\345\217\212\345\237\272\346\234\254\344\275\277\347\224\250.md" new file mode 100644 index 0000000..19e94d1 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Flume\347\256\200\344\273\213\345\217\212\345\237\272\346\234\254\344\275\277\347\224\250.md" @@ -0,0 +1,375 @@ +# Flume 简介及基本使用 + + + + +## 一、Flume简介 + +Apache Flume 是一个分布式,高可用的数据收集系统。它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集。Flume 分为 NG 和 OG (1.0 之前) 两个版本,NG 在 OG 的基础上进行了完全的重构,是目前使用最为广泛的版本。下面的介绍均以 NG 为基础。 + +## 二、Flume架构和基本概念 + +下图为 Flume 的基本架构图: + + + +
+ +### 2.1 基本架构 + +外部数据源以特定格式向 Flume 发送 `events` (事件),当 `source` 接收到 `events` 时,它将其存储到一个或多个 `channel`,`channe` 会一直保存 `events` 直到它被 `sink` 所消费。`sink` 的主要功能从 `channel` 中读取 `events`,并将其存入外部存储系统或转发到下一个 `source`,成功后再从 `channel` 中移除 `events`。 + + + +### 2.2 基本概念 + +**1. Event** + +`Event` 是 Flume NG 数据传输的基本单元。类似于 JMS 和消息系统中的消息。一个 `Event` 由标题和正文组成:前者是键/值映射,后者是任意字节数组。 + +**2. Source** + +数据收集组件,从外部数据源收集数据,并存储到 Channel 中。 + +**3. Channel** + +`Channel` 是源和接收器之间的管道,用于临时存储数据。可以是内存或持久化的文件系统: + ++ `Memory Channel` : 使用内存,优点是速度快,但数据可能会丢失 (如突然宕机); ++ `File Channel` : 使用持久化的文件系统,优点是能保证数据不丢失,但是速度慢。 + +**4. Sink** + +`Sink` 的主要功能从 `Channel` 中读取 `Event`,并将其存入外部存储系统或将其转发到下一个 `Source`,成功后再从 `Channel` 中移除 `Event`。 + +**5. Agent** + +是一个独立的 (JVM) 进程,包含 `Source`、 `Channel`、 `Sink` 等组件。 + + + +### 2.3 组件种类 + +Flume 中的每一个组件都提供了丰富的类型,适用于不同场景: + +- Source 类型 :内置了几十种类型,如 `Avro Source`,`Thrift Source`,`Kafka Source`,`JMS Source`; + +- Sink 类型 :`HDFS Sink`,`Hive Sink`,`HBaseSinks`,`Avro Sink` 等; + +- Channel 类型 :`Memory Channel`,`JDBC Channel`,`Kafka Channel`,`File Channel` 等。 + +对于 Flume 的使用,除非有特别的需求,否则通过组合内置的各种类型的 Source,Sink 和 Channel 就能满足大多数的需求。在 [Flume 官网](http://flume.apache.org/releases/content/1.9.0/FlumeUserGuide.html) 上对所有类型组件的配置参数均以表格的方式做了详尽的介绍,并附有配置样例;同时不同版本的参数可能略有所不同,所以使用时建议选取官网对应版本的 User Guide 作为主要参考资料。 + + + +## 三、Flume架构模式 + +Flume 支持多种架构模式,分别介绍如下 + +### 3.1 multi-agent flow + + + +
+ +
+ +Flume 支持跨越多个 Agent 的数据传递,这要求前一个 Agent 的 Sink 和下一个 Agent 的 Source 都必须是 `Avro` 类型,Sink 指向 Source 所在主机名 (或 IP 地址) 和端口(详细配置见下文案例三)。 + +### 3.2 Consolidation + +
+ + + +
+ +日志收集中常常存在大量的客户端(比如分布式 web 服务),Flume 支持使用多个 Agent 分别收集日志,然后通过一个或者多个 Agent 聚合后再存储到文件系统中。 + +### 3.3 Multiplexing the flow + +
+ +Flume 支持从一个 Source 向多个 Channel,也就是向多个 Sink 传递事件,这个操作称之为 `Fan Out`(扇出)。默认情况下 `Fan Out` 是向所有的 Channel 复制 `Event`,即所有 Channel 收到的数据都是相同的。同时 Flume 也支持在 `Source` 上自定义一个复用选择器 (multiplexing selector) 来实现自定义的路由规则。 + + + +## 四、Flume配置格式 + +Flume 配置通常需要以下两个步骤: + +1. 分别定义好 Agent 的 Sources,Sinks,Channels,然后将 Sources 和 Sinks 与通道进行绑定。需要注意的是一个 Source 可以配置多个 Channel,但一个 Sink 只能配置一个 Channel。基本格式如下: + +```shell +.sources = +.sinks = +.channels = + +# set channel for source +.sources..channels = ... + +# set channel for sink +.sinks..channel = +``` + +2. 分别定义 Source,Sink,Channel 的具体属性。基本格式如下: + +```shell + +.sources.. = + +# properties for channels +.channel.. = + +# properties for sinks +.sources.. = +``` + + + +## 五、Flume的安装部署 + +为方便大家后期查阅,本仓库中所有软件的安装均单独成篇,Flume 的安装见: + +[Linux 环境下 Flume 的安装部署](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux%E4%B8%8BFlume%E7%9A%84%E5%AE%89%E8%A3%85.md) + + + +## 六、Flume使用案例 + +介绍几个 Flume 的使用案例: + ++ 案例一:使用 Flume 监听文件内容变动,将新增加的内容输出到控制台。 ++ 案例二:使用 Flume 监听指定目录,将目录下新增加的文件存储到 HDFS。 ++ 案例三:使用 Avro 将本服务器收集到的日志数据发送到另外一台服务器。 + +### 6.1 案例一 + +需求: 监听文件内容变动,将新增加的内容输出到控制台。 + +实现: 主要使用 `Exec Source` 配合 `tail` 命令实现。 + +#### 1. 配置 + +新建配置文件 `exec-memory-logger.properties`,其内容如下: + +```properties +#指定agent的sources,sinks,channels +a1.sources = s1 +a1.sinks = k1 +a1.channels = c1 + +#配置sources属性 +a1.sources.s1.type = exec +a1.sources.s1.command = tail -F /tmp/log.txt +a1.sources.s1.shell = /bin/bash -c + +#将sources与channels进行绑定 +a1.sources.s1.channels = c1 + +#配置sink +a1.sinks.k1.type = logger + +#将sinks与channels进行绑定 +a1.sinks.k1.channel = c1 + +#配置channel类型 +a1.channels.c1.type = memory +``` + +#### 2. 启动  + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/exec-memory-logger.properties \ +--name a1 \ +-Dflume.root.logger=INFO,console +``` + +#### 3. 测试 + +向文件中追加数据: + +
+ +控制台的显示: + +
+ + + +### 6.2 案例二 + +需求: 监听指定目录,将目录下新增加的文件存储到 HDFS。 + +实现:使用 `Spooling Directory Source` 和 `HDFS Sink`。 + +#### 1. 配置 + +```properties +#指定agent的sources,sinks,channels +a1.sources = s1 +a1.sinks = k1 +a1.channels = c1 + +#配置sources属性 +a1.sources.s1.type =spooldir +a1.sources.s1.spoolDir =/tmp/logs +a1.sources.s1.basenameHeader = true +a1.sources.s1.basenameHeaderKey = fileName +#将sources与channels进行绑定 +a1.sources.s1.channels =c1 + + +#配置sink +a1.sinks.k1.type = hdfs +a1.sinks.k1.hdfs.path = /flume/events/%y-%m-%d/%H/ +a1.sinks.k1.hdfs.filePrefix = %{fileName} +#生成的文件类型,默认是Sequencefile,可用DataStream,则为普通文本 +a1.sinks.k1.hdfs.fileType = DataStream +a1.sinks.k1.hdfs.useLocalTimeStamp = true +#将sinks与channels进行绑定 +a1.sinks.k1.channel = c1 + +#配置channel类型 +a1.channels.c1.type = memory +``` + +#### 2. 启动 + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/spooling-memory-hdfs.properties \ +--name a1 -Dflume.root.logger=INFO,console +``` + +#### 3. 测试 + +拷贝任意文件到监听目录下,可以从日志看到文件上传到 HDFS 的路径: + +```shell +# cp log.txt logs/ +``` + +
+ +查看上传到 HDFS 上的文件内容与本地是否一致: + +```shell +# hdfs dfs -cat /flume/events/19-04-09/13/log.txt.1554788567801 +``` + +
+ + + +### 6.3 案例三 + +需求: 将本服务器收集到的数据发送到另外一台服务器。 + +实现:使用 `avro sources` 和 `avro Sink` 实现。 + +#### 1. 配置日志收集Flume + +新建配置 `netcat-memory-avro.properties`,监听文件内容变化,然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口: + +```properties +#指定agent的sources,sinks,channels +a1.sources = s1 +a1.sinks = k1 +a1.channels = c1 + +#配置sources属性 +a1.sources.s1.type = exec +a1.sources.s1.command = tail -F /tmp/log.txt +a1.sources.s1.shell = /bin/bash -c +a1.sources.s1.channels = c1 + +#配置sink +a1.sinks.k1.type = avro +a1.sinks.k1.hostname = hadoop001 +a1.sinks.k1.port = 8888 +a1.sinks.k1.batch-size = 1 +a1.sinks.k1.channel = c1 + +#配置channel类型 +a1.channels.c1.type = memory +a1.channels.c1.capacity = 1000 +a1.channels.c1.transactionCapacity = 100 +``` + +#### 2. 配置日志聚合Flume + +使用 `avro source` 监听 hadoop001 服务器的 8888 端口,将获取到内容输出到控制台: + +```properties +#指定agent的sources,sinks,channels +a2.sources = s2 +a2.sinks = k2 +a2.channels = c2 + +#配置sources属性 +a2.sources.s2.type = avro +a2.sources.s2.bind = hadoop001 +a2.sources.s2.port = 8888 + +#将sources与channels进行绑定 +a2.sources.s2.channels = c2 + +#配置sink +a2.sinks.k2.type = logger + +#将sinks与channels进行绑定 +a2.sinks.k2.channel = c2 + +#配置channel类型 +a2.channels.c2.type = memory +a2.channels.c2.capacity = 1000 +a2.channels.c2.transactionCapacity = 100 +``` + +#### 3. 启动 + +启动日志聚集 Flume: + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/avro-memory-logger.properties \ +--name a2 -Dflume.root.logger=INFO,console +``` + +在启动日志收集 Flume: + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-avro.properties \ +--name a1 -Dflume.root.logger=INFO,console +``` + +这里建议按以上顺序启动,原因是 `avro.source` 会先与端口进行绑定,这样 `avro sink` 连接时才不会报无法连接的异常。但是即使不按顺序启动也是没关系的,`sink` 会一直重试,直至建立好连接。 + +
+ +#### 4.测试 + +向文件 `tmp/log.txt` 中追加内容: + +
+ +可以看到已经从 8888 端口监听到内容,并成功输出到控制台: + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS-Java-API.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS-Java-API.md" new file mode 100644 index 0000000..f5ec70d --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS-Java-API.md" @@ -0,0 +1,388 @@ +# HDFS Java API + + + +## 一、 简介 + +想要使用 HDFS API,需要导入依赖 `hadoop-client`。如果是 CDH 版本的 Hadoop,还需要额外指明其仓库地址: + +```xml + + + 4.0.0 + + com.heibaiying + hdfs-java-api + 1.0 + + + + UTF-8 + 2.6.0-cdh5.15.2 + + + + + + + cloudera + https://repository.cloudera.com/artifactory/cloudera-repos/ + + + + + + + + org.apache.hadoop + hadoop-client + ${hadoop.version} + + + junit + junit + 4.12 + test + + + + +``` + + + +## 二、API的使用 + +### 2.1 FileSystem + +FileSystem 是所有 HDFS 操作的主入口。由于之后的每个单元测试都需要用到它,这里使用 `@Before` 注解进行标注。 + +```java +private static final String HDFS_PATH = "hdfs://192.168.0.106:8020"; +private static final String HDFS_USER = "root"; +private static FileSystem fileSystem; + +@Before +public void prepare() { + try { + Configuration configuration = new Configuration(); + // 这里我启动的是单节点的 Hadoop,所以副本系数设置为 1,默认值为 3 + configuration.set("dfs.replication", "1"); + fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, HDFS_USER); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } +} + + +@After +public void destroy() { + fileSystem = null; +} +``` + + + +### 2.2 创建目录 + +支持递归创建目录: + +```java +@Test +public void mkDir() throws Exception { + fileSystem.mkdirs(new Path("/hdfs-api/test0/")); +} +``` + + + +### 2.3 创建指定权限的目录 + +`FsPermission(FsAction u, FsAction g, FsAction o)` 的三个参数分别对应:创建者权限,同组其他用户权限,其他用户权限,权限值定义在 `FsAction` 枚举类中。 + +```java +@Test +public void mkDirWithPermission() throws Exception { + fileSystem.mkdirs(new Path("/hdfs-api/test1/"), + new FsPermission(FsAction.READ_WRITE, FsAction.READ, FsAction.READ)); +} +``` + + + +### 2.4 创建文件,并写入内容 + +```java +@Test +public void create() throws Exception { + // 如果文件存在,默认会覆盖, 可以通过第二个参数进行控制。第三个参数可以控制使用缓冲区的大小 + FSDataOutputStream out = fileSystem.create(new Path("/hdfs-api/test/a.txt"), + true, 4096); + out.write("hello hadoop!".getBytes()); + out.write("hello spark!".getBytes()); + out.write("hello flink!".getBytes()); + // 强制将缓冲区中内容刷出 + out.flush(); + out.close(); +} +``` + + + +### 2.5 判断文件是否存在 + +```java +@Test +public void exist() throws Exception { + boolean exists = fileSystem.exists(new Path("/hdfs-api/test/a.txt")); + System.out.println(exists); +} +``` + + + +### 2.6 查看文件内容 + +查看小文本文件的内容,直接转换成字符串后输出: + +```java +@Test +public void readToString() throws Exception { + FSDataInputStream inputStream = fileSystem.open(new Path("/hdfs-api/test/a.txt")); + String context = inputStreamToString(inputStream, "utf-8"); + System.out.println(context); +} +``` + +`inputStreamToString` 是一个自定义方法,代码如下: + +```java +/** + * 把输入流转换为指定编码的字符 + * + * @param inputStream 输入流 + * @param encode 指定编码类型 + */ +private static String inputStreamToString(InputStream inputStream, String encode) { + try { + if (encode == null || ("".equals(encode))) { + encode = "utf-8"; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, encode)); + StringBuilder builder = new StringBuilder(); + String str = ""; + while ((str = reader.readLine()) != null) { + builder.append(str).append("\n"); + } + return builder.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; +} +``` + + + +### 2.7 文件重命名 + +```java +@Test +public void rename() throws Exception { + Path oldPath = new Path("/hdfs-api/test/a.txt"); + Path newPath = new Path("/hdfs-api/test/b.txt"); + boolean result = fileSystem.rename(oldPath, newPath); + System.out.println(result); +} +``` + + + +### 2.8 删除目录或文件 + +```java +public void delete() throws Exception { + /* + * 第二个参数代表是否递归删除 + * + 如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件; + * + 如果 path 是一个目录但递归删除为 false,则会则抛出异常。 + */ + boolean result = fileSystem.delete(new Path("/hdfs-api/test/b.txt"), true); + System.out.println(result); +} +``` + + + +### 2.9 上传文件到HDFS + +```java +@Test +public void copyFromLocalFile() throws Exception { + // 如果指定的是目录,则会把目录及其中的文件都复制到指定目录下 + Path src = new Path("D:\\BigData-Notes\\notes\\installation"); + Path dst = new Path("/hdfs-api/test/"); + fileSystem.copyFromLocalFile(src, dst); +} +``` + + + +### 2.10 上传大文件并显示上传进度 + +```java +@Test + public void copyFromLocalBigFile() throws Exception { + + File file = new File("D:\\kafka.tgz"); + final float fileSize = file.length(); + InputStream in = new BufferedInputStream(new FileInputStream(file)); + + FSDataOutputStream out = fileSystem.create(new Path("/hdfs-api/test/kafka5.tgz"), + new Progressable() { + long fileCount = 0; + + public void progress() { + fileCount++; + // progress 方法每上传大约 64KB 的数据后就会被调用一次 + System.out.println("上传进度:" + (fileCount * 64 * 1024 / fileSize) * 100 + " %"); + } + }); + + IOUtils.copyBytes(in, out, 4096); + + } +``` + + + +### 2.11 从HDFS上下载文件 + +```java +@Test +public void copyToLocalFile() throws Exception { + Path src = new Path("/hdfs-api/test/kafka.tgz"); + Path dst = new Path("D:\\app\\"); + /* + * 第一个参数控制下载完成后是否删除源文件,默认是 true,即删除; + * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统; + * RawLocalFileSystem 默认为 false,通常情况下可以不设置, + * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见), + * 此时可以将 RawLocalFileSystem 设置为 true + */ + fileSystem.copyToLocalFile(false, src, dst, true); +} +``` + + + +### 2.12 查看指定目录下所有文件的信息 + +```java +public void listFiles() throws Exception { + FileStatus[] statuses = fileSystem.listStatus(new Path("/hdfs-api")); + for (FileStatus fileStatus : statuses) { + //fileStatus 的 toString 方法被重写过,直接打印可以看到所有信息 + System.out.println(fileStatus.toString()); + } +} +``` + +`FileStatus` 中包含了文件的基本信息,比如文件路径,是否是文件夹,修改时间,访问时间,所有者,所属组,文件权限,是否是符号链接等,输出内容示例如下: + +```properties +FileStatus{ +path=hdfs://192.168.0.106:8020/hdfs-api/test; +isDirectory=true; +modification_time=1556680796191; +access_time=0; +owner=root; +group=supergroup; +permission=rwxr-xr-x; +isSymlink=false +} +``` + + + +### 2.13 递归查看指定目录下所有文件的信息 + +```java +@Test +public void listFilesRecursive() throws Exception { + RemoteIterator files = fileSystem.listFiles(new Path("/hbase"), true); + while (files.hasNext()) { + System.out.println(files.next()); + } +} +``` + +和上面输出类似,只是多了文本大小,副本系数,块大小信息。 + +```properties +LocatedFileStatus{ +path=hdfs://192.168.0.106:8020/hbase/hbase.version; +isDirectory=false; +length=7; +replication=1; +blocksize=134217728; +modification_time=1554129052916; +access_time=1554902661455; +owner=root; group=supergroup; +permission=rw-r--r--; +isSymlink=false} +``` + + + +### 2.14 查看文件的块信息 + +```java +@Test +public void getFileBlockLocations() throws Exception { + + FileStatus fileStatus = fileSystem.getFileStatus(new Path("/hdfs-api/test/kafka.tgz")); + BlockLocation[] blocks = fileSystem.getFileBlockLocations(fileStatus, 0, fileStatus.getLen()); + for (BlockLocation block : blocks) { + System.out.println(block); + } +} +``` + +块输出信息有三个值,分别是文件的起始偏移量 (offset),文件大小 (length),块所在的主机名 (hosts)。 + +``` +0,57028557,hadoop001 +``` + +这里我上传的文件只有 57M(小于 128M),且程序中设置了副本系数为 1,所有只有一个块信息。 + +
+ +
+ +**以上所有测试用例下载地址**:[HDFS Java API](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hdfs-java-api) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" new file mode 100644 index 0000000..933eceb --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HDFS\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" @@ -0,0 +1,141 @@ +# HDFS 常用 shell 命令 + +**1. 显示当前目录结构** + +```shell +# 显示当前目录结构 +hadoop fs -ls +# 递归显示当前目录结构 +hadoop fs -ls -R +# 显示根目录下内容 +hadoop fs -ls / +``` + +**2. 创建目录** + +```shell +# 创建目录 +hadoop fs -mkdir +# 递归创建目录 +hadoop fs -mkdir -p +``` + +**3. 删除操作** + +```shell +# 删除文件 +hadoop fs -rm +# 递归删除目录和文件 +hadoop fs -rm -R +``` + +**4. 从本地加载文件到 HDFS** + +```shell +# 二选一执行即可 +hadoop fs -put [localsrc] [dst] +hadoop fs - copyFromLocal [localsrc] [dst] +``` + + +**5. 从 HDFS 导出文件到本地** + +```shell +# 二选一执行即可 +hadoop fs -get [dst] [localsrc] +hadoop fs -copyToLocal [dst] [localsrc] +``` + +**6. 查看文件内容** + +```shell +# 二选一执行即可 +hadoop fs -text +hadoop fs -cat +``` + +**7. 显示文件的最后一千字节** + +```shell +hadoop fs -tail +# 和Linux下一样,会持续监听文件内容变化 并显示文件的最后一千字节 +hadoop fs -tail -f +``` + +**8. 拷贝文件** + +```shell +hadoop fs -cp [src] [dst] +``` + +**9. 移动文件** + +```shell +hadoop fs -mv [src] [dst] +``` + + +**10. 统计当前目录下各文件大小** ++ 默认单位字节 ++ -s : 显示所有文件大小总和, ++ -h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864) +```shell +hadoop fs -du +``` + +**11. 合并下载多个文件** ++ -nl 在每个文件的末尾添加换行符(LF) ++ -skip-empty-file 跳过空文件 + +```shell +hadoop fs -getmerge +# 示例 将HDFS上的hbase-policy.xml和hbase-site.xml文件合并后下载到本地的/usr/test.xml +hadoop fs -getmerge -nl /test/hbase-policy.xml /test/hbase-site.xml /usr/test.xml +``` + +**12. 统计文件系统的可用空间信息** + +```shell +hadoop fs -df -h / +``` + +**13. 更改文件复制因子** +```shell +hadoop fs -setrep [-R] [-w] +``` ++ 更改文件的复制因子。如果 path 是目录,则更改其下所有文件的复制因子 ++ -w : 请求命令是否等待复制完成 + +```shell +# 示例 +hadoop fs -setrep -w 3 /user/hadoop/dir1 +``` + +**14. 权限控制** +```shell +# 权限控制和Linux上使用方式一致 +# 变更文件或目录的所属群组。 用户必须是文件的所有者或超级用户。 +hadoop fs -chgrp [-R] GROUP URI [URI ...] +# 修改文件或目录的访问权限 用户必须是文件的所有者或超级用户。 +hadoop fs -chmod [-R] URI [URI ...] +# 修改文件的拥有者 用户必须是超级用户。 +hadoop fs -chown [-R] [OWNER][:[GROUP]] URI [URI ] +``` + +**15. 文件检测** +```shell +hadoop fs -test - [defsz] URI +``` +可选选项: ++ -d:如果路径是目录,返回 0。 ++ -e:如果路径存在,则返回 0。 ++ -f:如果路径是文件,则返回 0。 ++ -s:如果路径不为空,则返回 0。 ++ -r:如果路径存在且授予读权限,则返回 0。 ++ -w:如果路径存在且授予写入权限,则返回 0。 ++ -z:如果文件长度为零,则返回 0。 + +```shell +# 示例 +hadoop fs -test -e filename +``` diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-HDFS.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-HDFS.md" new file mode 100644 index 0000000..a4826a6 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-HDFS.md" @@ -0,0 +1,176 @@ +# Hadoop分布式文件系统——HDFS + + + + + +## 一、介绍 + +**HDFS** (**Hadoop Distributed File System**)是 Hadoop 下的分布式文件系统,具有高容错、高吞吐量等特性,可以部署在低成本的硬件上。 + + + +## 二、HDFS 设计原理 + +
+ +### 2.1 HDFS 架构 + +HDFS 遵循主/从架构,由单个 NameNode(NN) 和多个 DataNode(DN) 组成: + +- **NameNode** : 负责执行有关 ` 文件系统命名空间 ` 的操作,例如打开,关闭、重命名文件和目录等。它同时还负责集群元数据的存储,记录着文件中各个数据块的位置信息。 +- **DataNode**:负责提供来自文件系统客户端的读写请求,执行块的创建,删除等操作。 + + + +### 2.2 文件系统命名空间 + +HDFS 的 ` 文件系统命名空间 ` 的层次结构与大多数文件系统类似 (如 Linux), 支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限,但不支持硬链接和软连接。`NameNode` 负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。 + + + +### 2.3 数据复制 + +由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列**块**,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是 128M,默认复制因子是 3)。 + +
+ +### 2.4 数据复制的实现原理 + +大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是: + +在写入程序位于 `datanode` 上时,就优先将写入文件的一个副本放置在该 `datanode` 上,否则放在随机 `datanode` 上。之后在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。 + +
+ +如果复制因子大于 3,则随机确定第 4 个和之后副本的放置位置,同时保持每个机架的副本数量低于上限,上限值通常为 `(复制系数 - 1)/机架数量 + 2`,需要注意的是不允许同一个 `dataNode` 上具有同一个块的多个副本。 + + + +### 2.5 副本的选择 + +为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。 + + + +### 2.6 架构的稳定性 + +#### 1. 心跳机制和重新复制 + +每个 DataNode 定期向 NameNode 发送心跳消息,如果超过指定时间没有收到心跳消息,则将 DataNode 标记为死亡。NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode,也不会再使用这些 DataNode 上的数据。 由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode 会跟踪这些块,并在必要的时候进行重新复制。 + +#### 2. 数据的完整性 + +由于存储设备故障等原因,存储在 DataNode 上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误,HDFS 提供了数据完整性校验机制来保证数据的完整性,具体操作如下: + +当客户端创建 HDFS 文件时,它会计算文件的每个块的 ` 校验和 `,并将 ` 校验和 ` 存储在同一 HDFS 命名空间下的单独的隐藏文件中。当客户端检索文件内容时,它会验证从每个 DataNode 接收的数据是否与存储在关联校验和文件中的 ` 校验和 ` 匹配。如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他 DataNode 获取该块的其他可用副本。 + +#### 3.元数据的磁盘故障 + +`FsImage` 和 `EditLog` 是 HDFS 的核心数据,这些数据的意外丢失可能会导致整个 HDFS 服务不可用。为了避免这个问题,可以配置 NameNode 使其支持 `FsImage` 和 `EditLog` 多副本同步,这样 `FsImage` 或 `EditLog` 的任何改变都会引起每个副本 `FsImage` 和 `EditLog` 的同步更新。 + +#### 4.支持快照 + +快照支持在特定时刻存储数据副本,在数据意外损坏时,可以通过回滚操作恢复到健康的数据状态。 + + + +## 三、HDFS 的特点 + +### 3.1 高容错 + +由于 HDFS 采用数据的多副本方案,所以部分硬件的损坏不会导致全部数据的丢失。 + +### 3.2 高吞吐量 + +HDFS 设计的重点是支持高吞吐量的数据访问,而不是低延迟的数据访问。 + +### 3.3 大文件支持 + +HDFS 适合于大文件的存储,文档的大小应该是是 GB 到 TB 级别的。 + +### 3.3 简单一致性模型 + +HDFS 更适合于一次写入多次读取 (write-once-read-many) 的访问模型。支持将内容追加到文件末尾,但不支持数据的随机访问,不能从文件任意位置新增数据。 + +### 3.4 跨平台移植性 + +HDFS 具有良好的跨平台移植性,这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。 + + + +## 附:图解HDFS存储原理 + +> 说明:以下图片引用自博客:[翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) + +### 1. HDFS写数据原理 + +
+ +
+ +
+ + + +### 2. HDFS读数据原理 + +
+ + + +### 3. HDFS故障类型和其检测方法 + +
+ +
+ + + +**第二部分:读写故障的处理** + +
+ + + +**第三部分:DataNode 故障处理** + +
+ + + +**副本布局策略**: + +
+ + + +## 参考资料 + +1. [Apache Hadoop 2.9.2 > HDFS Architecture](http://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) +2. Tom White . hadoop 权威指南 [M] . 清华大学出版社 . 2017. +3. [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-MapReduce.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-MapReduce.md" new file mode 100644 index 0000000..74f5ab9 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-MapReduce.md" @@ -0,0 +1,384 @@ +# 分布式计算框架——MapReduce + + + + + + +## 一、MapReduce概述 + +Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集。 + +MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 `map` 以并行的方式处理,框架对 `map` 的输出进行排序,然后输入到 `reduce` 中。MapReduce 框架专门用于 `` 键值对处理,它将作业的输入视为一组 `` 对,并生成一组 `` 对作为输出。输出和输出的 `key` 和 `value` 都必须实现[Writable](http://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。 + +``` +(input) -> map -> -> combine -> -> reduce -> (output) +``` + + + +## 二、MapReduce编程模型简述 + +这里以词频统计为例进行说明,MapReduce 处理的流程如下: + +
+ +1. **input** : 读取文本文件; + +2. **splitting** : 将文件按照行进行拆分,此时得到的 `K1` 行数,`V1` 表示对应行的文本内容; + +3. **mapping** : 并行将每一行按照空格进行拆分,拆分得到的 `List(K2,V2)`,其中 `K2` 代表每一个单词,由于是做词频统计,所以 `V2` 的值为 1,代表出现 1 次; +4. **shuffling**:由于 `Mapping` 操作可能是在不同的机器上并行处理的,所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 `K2` 为每一个单词,`List(V2)` 为可迭代集合,`V2` 就是 Mapping 中的 V2; +5. **Reducing** : 这里的案例是统计单词出现的总次数,所以 `Reducing` 对 `List(V2)` 进行归约求和操作,最终输出。 + +MapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的,需要我们自己编程实现的只有 `mapping` 和 `reducing`,这也就是 MapReduce 这个称呼的来源。 + + + +## 三、combiner & partitioner + +
+ +### 3.1 InputFormat & RecordReaders + +`InputFormat` 将输出文件拆分为多个 `InputSplit`,并由 `RecordReaders` 将 `InputSplit` 转换为标准的键值对,作为 map 的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后,才能为多个 `map` 提供输入,以便进行并行处理。 + + + +### 3.2 Combiner + +`combiner` 是 `map` 运算后的可选操作,它实际上是一个本地化的 `reduce` 操作,它主要是在 `map` 计算出中间文件后做一个简单的合并重复 `key` 值的操作。这里以词频统计为例: + +`map` 在遇到一个 hadoop 的单词时就会记录为 1,但是这篇文章里 hadoop 可能会出现 n 多次,那么 `map` 输出文件冗余就会很多,因此在 `reduce` 计算前对相同的 key 做一个合并操作,那么需要传输的数据量就会减少,传输效率就可以得到提升。 + +但并非所有场景都适合使用 `combiner`,使用它的原则是 `combiner` 的输出不会影响到 `reduce` 计算的最终输入,例如:求总数,最大值,最小值时都可以使用 `combiner`,但是做平均值计算则不能使用 `combiner`。 + +不使用 combiner 的情况: + +
+ +使用 combiner 的情况: + +
+ + + +可以看到使用 combiner 的时候,需要传输到 reducer 中的数据由 12keys,降低到 10keys。降低的幅度取决于你 keys 的重复率,下文词频统计案例会演示用 combiner 降低数百倍的传输量。 + +### 3.3 Partitioner + +`partitioner` 可以理解成分类器,将 `map` 的输出按照 key 值的不同分别分给对应的 `reducer`,支持自定义实现,下文案例会给出演示。 + + + +## 四、MapReduce词频统计案例 + +### 4.1 项目简介 + +这里给出一个经典的词频统计的案例:统计如下样本数据中每个单词出现的次数。 + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +``` + +为方便大家开发,我在项目源码中放置了一个工具类 `WordCountDataUtils`,用于模拟产生词频统计的样本,生成的文件支持输出到本地或者直接写到 HDFS 上。 + +> 项目完整源码下载地址:[hadoop-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hadoop-word-count) + + + +### 4.2 项目依赖 + +想要进行 MapReduce 编程,需要导入 `hadoop-client` 依赖: + +```xml + + org.apache.hadoop + hadoop-client + ${hadoop.version} + +``` + +### 4.3 WordCountMapper + +将每行数据按照指定分隔符进行拆分。这里需要注意在 MapReduce 中必须使用 Hadoop 定义的类型,因为 Hadoop 预定义的类型都是可序列化,可比较的,所有类型均实现了 `WritableComparable` 接口。 + +```java +public class WordCountMapper extends Mapper { + + @Override + protected void map(LongWritable key, Text value, Context context) throws IOException, + InterruptedException { + String[] words = value.toString().split("\t"); + for (String word : words) { + context.write(new Text(word), new IntWritable(1)); + } + } + +} +``` + +`WordCountMapper` 对应下图的 Mapping 操作: + +
+ + + +`WordCountMapper` 继承自 `Mappe` 类,这是一个泛型类,定义如下: + +```java +WordCountMapper extends Mapper + +public class Mapper { + ...... +} +``` + ++ **KEYIN** : `mapping` 输入 key 的类型,即每行的偏移量 (每行第一个字符在整个文本中的位置),`Long` 类型,对应 Hadoop 中的 `LongWritable` 类型; ++ **VALUEIN** : `mapping` 输入 value 的类型,即每行数据;`String` 类型,对应 Hadoop 中 `Text` 类型; ++ **KEYOUT** :`mapping` 输出的 key 的类型,即每个单词;`String` 类型,对应 Hadoop 中 `Text` 类型; ++ **VALUEOUT**:`mapping` 输出 value 的类型,即每个单词出现的次数;这里用 `int` 类型,对应 `IntWritable` 类型。 + + + +### 4.4 WordCountReducer + +在 Reduce 中进行单词出现次数的统计: + +```java +public class WordCountReducer extends Reducer { + + @Override + protected void reduce(Text key, Iterable values, Context context) throws IOException, + InterruptedException { + int count = 0; + for (IntWritable value : values) { + count += value.get(); + } + context.write(key, new IntWritable(count)); + } +} +``` + +如下图,`shuffling` 的输出是 reduce 的输入。这里的 key 是每个单词,values 是一个可迭代的数据类型,类似 `(1,1,1,...)`。 + +
+ +### 4.4 WordCountApp + +组装 MapReduce 作业,并提交到服务器运行,代码如下: + +```java + +/** + * 组装作业 并提交到集群运行 + */ +public class WordCountApp { + + + // 这里为了直观显示参数 使用了硬编码,实际开发中可以通过外部传参 + private static final String HDFS_URL = "hdfs://192.168.0.107:8020"; + private static final String HADOOP_USER_NAME = "root"; + + public static void main(String[] args) throws Exception { + + // 文件输入路径和输出路径由外部传参指定 + if (args.length < 2) { + System.out.println("Input and output paths are necessary!"); + return; + } + + // 需要指明 hadoop 用户名,否则在 HDFS 上创建目录时可能会抛出权限不足的异常 + System.setProperty("HADOOP_USER_NAME", HADOOP_USER_NAME); + + Configuration configuration = new Configuration(); + // 指明 HDFS 的地址 + configuration.set("fs.defaultFS", HDFS_URL); + + // 创建一个 Job + Job job = Job.getInstance(configuration); + + // 设置运行的主类 + job.setJarByClass(WordCountApp.class); + + // 设置 Mapper 和 Reducer + job.setMapperClass(WordCountMapper.class); + job.setReducerClass(WordCountReducer.class); + + // 设置 Mapper 输出 key 和 value 的类型 + job.setMapOutputKeyClass(Text.class); + job.setMapOutputValueClass(IntWritable.class); + + // 设置 Reducer 输出 key 和 value 的类型 + job.setOutputKeyClass(Text.class); + job.setOutputValueClass(IntWritable.class); + + // 如果输出目录已经存在,则必须先删除,否则重复运行程序时会抛出异常 + FileSystem fileSystem = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME); + Path outputPath = new Path(args[1]); + if (fileSystem.exists(outputPath)) { + fileSystem.delete(outputPath, true); + } + + // 设置作业输入文件和输出文件的路径 + FileInputFormat.setInputPaths(job, new Path(args[0])); + FileOutputFormat.setOutputPath(job, outputPath); + + // 将作业提交到群集并等待它完成,参数设置为 true 代表打印显示对应的进度 + boolean result = job.waitForCompletion(true); + + // 关闭之前创建的 fileSystem + fileSystem.close(); + + // 根据作业结果,终止当前运行的 Java 虚拟机,退出程序 + System.exit(result ? 0 : -1); + + } +} +``` + +需要注意的是:如果不设置 `Mapper` 操作的输出类型,则程序默认它和 `Reducer` 操作输出的类型相同。 + +### 4.5 提交到服务器运行 + +在实际开发中,可以在本机配置 hadoop 开发环境,直接在 IDE 中启动进行测试。这里主要介绍一下打包提交到服务器运行。由于本项目没有使用除 Hadoop 外的第三方依赖,直接打包即可: + +```shell +# mvn clean package +``` + +使用以下命令提交作业: + +```shell +hadoop jar /usr/appjar/hadoop-word-count-1.0.jar \ +com.heibaiying.WordCountApp \ +/wordcount/input.txt /wordcount/output/WordCountApp +``` + +作业完成后查看 HDFS 上生成目录: + +```shell +# 查看目录 +hadoop fs -ls /wordcount/output/WordCountApp + +# 查看统计结果 +hadoop fs -cat /wordcount/output/WordCountApp/part-r-00000 +``` + +
+ + + +## 五、词频统计案例进阶之Combiner + +### 5.1 代码实现 + +想要使用 `combiner` 功能只要在组装作业时,添加下面一行代码即可: + +```java +// 设置 Combiner +job.setCombinerClass(WordCountReducer.class); +``` + +### 5.2 执行结果 + +加入 `combiner` 后统计结果是不会有变化的,但是可以从打印的日志看出 `combiner` 的效果: + +没有加入 `combiner` 的打印日志: + +
+ +加入 `combiner` 后的打印日志如下: + +
+ +这里我们只有一个输入文件并且小于 128M,所以只有一个 Map 进行处理。可以看到经过 combiner 后,records 由 `3519` 降低为 `6`(样本中单词种类就只有 6 种),在这个用例中 combiner 就能极大地降低需要传输的数据量。 + + + +## 六、词频统计案例进阶之Partitioner + +### 6.1 默认的Partitioner + +这里假设有个需求:将不同单词的统计结果输出到不同文件。这种需求实际上比较常见,比如统计产品的销量时,需要将结果按照产品种类进行拆分。要实现这个功能,就需要用到自定义 `Partitioner`。 + +这里先介绍下 MapReduce 默认的分类规则:在构建 job 时候,如果不指定,默认的使用的是 `HashPartitioner`:对 key 值进行哈希散列并对 `numReduceTasks` 取余。其实现如下: + +```java +public class HashPartitioner extends Partitioner { + + public int getPartition(K key, V value, + int numReduceTasks) { + return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; + } + +} +``` + +### 6.2 自定义Partitioner + +这里我们继承 `Partitioner` 自定义分类规则,这里按照单词进行分类: + +```java +public class CustomPartitioner extends Partitioner { + + public int getPartition(Text text, IntWritable intWritable, int numPartitions) { + return WordCountDataUtils.WORD_LIST.indexOf(text.toString()); + } +} +``` + +在构建 `job` 时候指定使用我们自己的分类规则,并设置 `reduce` 的个数: + +```java +// 设置自定义分区规则 +job.setPartitionerClass(CustomPartitioner.class); +// 设置 reduce 个数 +job.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size()); +``` + + + +### 6.3 执行结果 + +执行结果如下,分别生成 6 个文件,每个文件中为对应单词的统计结果: + +
+ + + + + +## 参考资料 + +1. [分布式计算框架 MapReduce](https://zhuanlan.zhihu.com/p/28682581) +2. [Apache Hadoop 2.9.2 > MapReduce Tutorial](http://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html) +3. [MapReduce - Combiners]( https://www.tutorialscampus.com/tutorials/map-reduce/combiners.htm) + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-YARN.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-YARN.md" new file mode 100644 index 0000000..91e879e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hadoop-YARN.md" @@ -0,0 +1,128 @@ +# 集群资源管理器——YARN + + + + + +## 一、hadoop yarn 简介 + +**Apache YARN** (Yet Another Resource Negotiator) 是 hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。 + +
+ + + +## 二、YARN架构 + +
+ +### 1. ResourceManager + +`ResourceManager` 通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。`ResourceManager` 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源。 + +### 2. NodeManager + +`NodeManager` 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下: + +- 启动时向 `ResourceManager` 注册并定时发送心跳消息,等待 `ResourceManager` 的指令; +- 维护 `Container` 的生命周期,监控 `Container` 的资源使用情况; +- 管理任务运行时的相关依赖,根据 `ApplicationMaster` 的需要,在启动 `Container` 之前将需要的程序及其依赖拷贝到本地。 + +### 3. ApplicationMaster + +在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 `ApplicationMaster`。`ApplicationMaster` 负责协调来自 `ResourceManager` 的资源,并通过 `NodeManager` 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下: + +- 根据应用的运行状态来决定动态计算资源需求; +- 向 `ResourceManager` 申请资源,监控申请的资源的使用情况; +- 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息; +- 负责任务的容错。 + +### 4. Contain + +`Container` 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 `Container` 表示的。YARN 会为每个任务分配一个 `Container`,该任务只能使用该 `Container` 中描述的资源。`ApplicationMaster` 可在 `Container` 内运行任何类型的任务。例如,`MapReduce ApplicationMaster` 请求一个容器来启动 map 或 reduce 任务,而 `Giraph ApplicationMaster` 请求一个容器来运行 Giraph 任务。 + + + + + +## 三、YARN工作原理简述 + +
+ +1. `Client` 提交作业到 YARN 上; + +2. `Resource Manager` 选择一个 `Node Manager`,启动一个 `Container` 并运行 `Application Master` 实例; + +3. `Application Master` 根据实际需要向 `Resource Manager` 请求更多的 `Container` 资源(如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务); + +4. `Application Master` 通过获取到的 `Container` 资源执行分布式计算。 + + + +## 四、YARN工作原理详述 + +
+ + + +#### 1. 作业提交 + +client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出, 计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件, split 信息) 拷贝给 HDFS(第 3 步)。 最后, 通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。 + +#### 2. 作业初始化 + +当资源管理器收到 submitApplciation() 的请求时, 就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程, 由节点管理器监控 (第 5 步)。 + +MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度, 得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步),然后为每个输入 split 创建一个 map 任务, 根据 mapreduce.job.reduces 创建 reduce 任务对象。 + +#### 3. 任务分配 + +如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务。 + +如果不是小作业, 那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的, 包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点, 或者分配给和存放输入 split 的节点相同机架的节点。 + +#### 4. 任务运行 + +当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR 文件, 以及分布式缓存的所有文件 (第 10 步。 最后, 运行 map 或 reduce 任务 (第 11 步)。 + +YarnChild 运行在一个专用的 JVM 中, 但是 YARN 不支持 JVM 重用。 + +#### 5. 进度和状态更新 + +YARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器, 客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新, 展示给用户。 + +#### 6. 作业完成 + +除了向应用管理器请求作业进度外, 客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后, 应用管理器和 container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。 + + + +## 五、提交作业到YARN上运行 + +这里以提交 Hadoop Examples 中计算 Pi 的 MApReduce 程序为例,相关 Jar 包在 Hadoop 安装目录的 `share/hadoop/mapreduce` 目录下: + +```shell +# 提交格式: hadoop jar jar包路径 主类名称 主类参数 +# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 +``` + + + +## 参考资料 + +1. [初步掌握 Yarn 的架构及原理](https://www.cnblogs.com/codeOfLife/p/5492740.html) + +2. [Apache Hadoop 2.9.2 > Apache Hadoop YARN](http://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Java_API.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Java_API.md" new file mode 100644 index 0000000..d73cb58 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Java_API.md" @@ -0,0 +1,761 @@ +# HBase Java API 的基本使用 + + + + + +## 一、简述 + +截至到目前 (2019.04),HBase 有两个主要的版本,分别是 1.x 和 2.x ,两个版本的 Java API 有所不同,1.x 中某些方法在 2.x 中被标识为 `@deprecated` 过时。所以下面关于 API 的样例,我会分别给出 1.x 和 2.x 两个版本。完整的代码见本仓库: + +>+ [Java API 1.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-1.x) +> +>+ [Java API 2.x Examples](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase/hbase-java-api-2.x) + +同时你使用的客户端的版本必须与服务端版本保持一致,如果用 2.x 版本的客户端代码去连接 1.x 版本的服务端,会抛出 `NoSuchColumnFamilyException` 等异常。 + +## 二、Java API 1.x 基本使用 + +#### 2.1 新建Maven工程,导入项目依赖 + +要使用 Java API 操作 HBase,需要引入 `hbase-client`。这里选取的 `HBase Client` 的版本为 `1.2.0`。 + +```xml + + org.apache.hbase + hbase-client + 1.2.0 + +``` + +#### 2.2 API 基本使用 + +```java +public class HBaseUtils { + + private static Connection connection; + + static { + Configuration configuration = HBaseConfiguration.create(); + configuration.set("hbase.zookeeper.property.clientPort", "2181"); + // 如果是集群 则主机名用逗号分隔 + configuration.set("hbase.zookeeper.quorum", "hadoop001"); + try { + connection = ConnectionFactory.createConnection(configuration); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 创建 HBase 表 + * + * @param tableName 表名 + * @param columnFamilies 列族的数组 + */ + public static boolean createTable(String tableName, List columnFamilies) { + try { + HBaseAdmin admin = (HBaseAdmin) connection.getAdmin(); + if (admin.tableExists(tableName)) { + return false; + } + HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf(tableName)); + columnFamilies.forEach(columnFamily -> { + HColumnDescriptor columnDescriptor = new HColumnDescriptor(columnFamily); + columnDescriptor.setMaxVersions(1); + tableDescriptor.addFamily(columnDescriptor); + }); + admin.createTable(tableDescriptor); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 删除 hBase 表 + * + * @param tableName 表名 + */ + public static boolean deleteTable(String tableName) { + try { + HBaseAdmin admin = (HBaseAdmin) connection.getAdmin(); + // 删除表前需要先禁用表 + admin.disableTable(tableName); + admin.deleteTable(tableName); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + + /** + * 插入数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamilyName 列族名 + * @param qualifier 列标识 + * @param value 数据 + */ + public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier, + String value) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Put put = new Put(Bytes.toBytes(rowKey)); + put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value)); + table.put(put); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 插入数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamilyName 列族名 + * @param pairList 列标识和值的集合 + */ + public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List> pairList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Put put = new Put(Bytes.toBytes(rowKey)); + pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getKey()), Bytes.toBytes(pair.getValue()))); + table.put(put); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 根据 rowKey 获取指定行的数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + */ + public static Result getRow(String tableName, String rowKey) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Get get = new Get(Bytes.toBytes(rowKey)); + return table.get(get); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 获取指定行指定列 (cell) 的最新版本的数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamily 列族 + * @param qualifier 列标识 + */ + public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Get get = new Get(Bytes.toBytes(rowKey)); + if (!get.isCheckExistenceOnly()) { + get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier)); + Result result = table.get(get); + byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier)); + return Bytes.toString(resultValue); + } else { + return null; + } + + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 检索全表 + * + * @param tableName 表名 + */ + public static ResultScanner getScanner(String tableName) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 检索表中指定数据 + * + * @param tableName 表名 + * @param filterList 过滤器 + */ + + public static ResultScanner getScanner(String tableName, FilterList filterList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + scan.setFilter(filterList); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 检索表中指定数据 + * + * @param tableName 表名 + * @param startRowKey 起始 RowKey + * @param endRowKey 终止 RowKey + * @param filterList 过滤器 + */ + + public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey, + FilterList filterList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + scan.setStartRow(Bytes.toBytes(startRowKey)); + scan.setStopRow(Bytes.toBytes(endRowKey)); + scan.setFilter(filterList); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 删除指定行记录 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + */ + public static boolean deleteRow(String tableName, String rowKey) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Delete delete = new Delete(Bytes.toBytes(rowKey)); + table.delete(delete); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 删除指定行的指定列 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param familyName 列族 + * @param qualifier 列标识 + */ + public static boolean deleteColumn(String tableName, String rowKey, String familyName, + String qualifier) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Delete delete = new Delete(Bytes.toBytes(rowKey)); + delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier)); + table.delete(delete); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + +} +``` + +### 2.3 单元测试 + +以单元测试的方式对上面封装的 API 进行测试。 + +```java +public class HBaseUtilsTest { + + private static final String TABLE_NAME = "class"; + private static final String TEACHER = "teacher"; + private static final String STUDENT = "student"; + + @Test + public void createTable() { + // 新建表 + List columnFamilies = Arrays.asList(TEACHER, STUDENT); + boolean table = HBaseUtils.createTable(TABLE_NAME, columnFamilies); + System.out.println("表创建结果:" + table); + } + + @Test + public void insertData() { + List> pairs1 = Arrays.asList(new Pair<>("name", "Tom"), + new Pair<>("age", "22"), + new Pair<>("gender", "1")); + HBaseUtils.putRow(TABLE_NAME, "rowKey1", STUDENT, pairs1); + + List> pairs2 = Arrays.asList(new Pair<>("name", "Jack"), + new Pair<>("age", "33"), + new Pair<>("gender", "2")); + HBaseUtils.putRow(TABLE_NAME, "rowKey2", STUDENT, pairs2); + + List> pairs3 = Arrays.asList(new Pair<>("name", "Mike"), + new Pair<>("age", "44"), + new Pair<>("gender", "1")); + HBaseUtils.putRow(TABLE_NAME, "rowKey3", STUDENT, pairs3); + } + + + @Test + public void getRow() { + Result result = HBaseUtils.getRow(TABLE_NAME, "rowKey1"); + if (result != null) { + System.out.println(Bytes + .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes("name")))); + } + + } + + @Test + public void getCell() { + String cell = HBaseUtils.getCell(TABLE_NAME, "rowKey2", STUDENT, "age"); + System.out.println("cell age :" + cell); + + } + + @Test + public void getScanner() { + ResultScanner scanner = HBaseUtils.getScanner(TABLE_NAME); + if (scanner != null) { + scanner.forEach(result -> System.out.println(Bytes.toString(result.getRow()) + "->" + Bytes + .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes("name"))))); + scanner.close(); + } + } + + + @Test + public void getScannerWithFilter() { + FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL); + SingleColumnValueFilter nameFilter = new SingleColumnValueFilter(Bytes.toBytes(STUDENT), + Bytes.toBytes("name"), CompareOperator.EQUAL, Bytes.toBytes("Jack")); + filterList.addFilter(nameFilter); + ResultScanner scanner = HBaseUtils.getScanner(TABLE_NAME, filterList); + if (scanner != null) { + scanner.forEach(result -> System.out.println(Bytes.toString(result.getRow()) + "->" + Bytes + .toString(result.getValue(Bytes.toBytes(STUDENT), Bytes.toBytes("name"))))); + scanner.close(); + } + } + + @Test + public void deleteColumn() { + boolean b = HBaseUtils.deleteColumn(TABLE_NAME, "rowKey2", STUDENT, "age"); + System.out.println("删除结果: " + b); + } + + @Test + public void deleteRow() { + boolean b = HBaseUtils.deleteRow(TABLE_NAME, "rowKey2"); + System.out.println("删除结果: " + b); + } + + @Test + public void deleteTable() { + boolean b = HBaseUtils.deleteTable(TABLE_NAME); + System.out.println("删除结果: " + b); + } +} +``` + + + +## 三、Java API 2.x 基本使用 + +#### 3.1 新建Maven工程,导入项目依赖 + +这里选取的 `HBase Client` 的版本为最新的 `2.1.4`。 + +```xml + + org.apache.hbase + hbase-client + 2.1.4 + +``` + +#### 3.2 API 的基本使用 + +2.x 版本相比于 1.x 废弃了一部分方法,关于废弃的方法在源码中都会指明新的替代方法,比如,在 2.x 中创建表时:`HTableDescriptor` 和 `HColumnDescriptor` 等类都标识为废弃,取而代之的是使用 `TableDescriptorBuilder` 和 `ColumnFamilyDescriptorBuilder` 来定义表和列族。 + +
+ + + +以下为 HBase 2.x 版本 Java API 的使用示例: + +```java +public class HBaseUtils { + + private static Connection connection; + + static { + Configuration configuration = HBaseConfiguration.create(); + configuration.set("hbase.zookeeper.property.clientPort", "2181"); + // 如果是集群 则主机名用逗号分隔 + configuration.set("hbase.zookeeper.quorum", "hadoop001"); + try { + connection = ConnectionFactory.createConnection(configuration); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 创建 HBase 表 + * + * @param tableName 表名 + * @param columnFamilies 列族的数组 + */ + public static boolean createTable(String tableName, List columnFamilies) { + try { + HBaseAdmin admin = (HBaseAdmin) connection.getAdmin(); + if (admin.tableExists(TableName.valueOf(tableName))) { + return false; + } + TableDescriptorBuilder tableDescriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName)); + columnFamilies.forEach(columnFamily -> { + ColumnFamilyDescriptorBuilder cfDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily)); + cfDescriptorBuilder.setMaxVersions(1); + ColumnFamilyDescriptor familyDescriptor = cfDescriptorBuilder.build(); + tableDescriptor.setColumnFamily(familyDescriptor); + }); + admin.createTable(tableDescriptor.build()); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 删除 hBase 表 + * + * @param tableName 表名 + */ + public static boolean deleteTable(String tableName) { + try { + HBaseAdmin admin = (HBaseAdmin) connection.getAdmin(); + // 删除表前需要先禁用表 + admin.disableTable(TableName.valueOf(tableName)); + admin.deleteTable(TableName.valueOf(tableName)); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + + /** + * 插入数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamilyName 列族名 + * @param qualifier 列标识 + * @param value 数据 + */ + public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier, + String value) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Put put = new Put(Bytes.toBytes(rowKey)); + put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value)); + table.put(put); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 插入数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamilyName 列族名 + * @param pairList 列标识和值的集合 + */ + public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List> pairList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Put put = new Put(Bytes.toBytes(rowKey)); + pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getKey()), Bytes.toBytes(pair.getValue()))); + table.put(put); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 根据 rowKey 获取指定行的数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + */ + public static Result getRow(String tableName, String rowKey) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Get get = new Get(Bytes.toBytes(rowKey)); + return table.get(get); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 获取指定行指定列 (cell) 的最新版本的数据 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param columnFamily 列族 + * @param qualifier 列标识 + */ + public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Get get = new Get(Bytes.toBytes(rowKey)); + if (!get.isCheckExistenceOnly()) { + get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier)); + Result result = table.get(get); + byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier)); + return Bytes.toString(resultValue); + } else { + return null; + } + + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 检索全表 + * + * @param tableName 表名 + */ + public static ResultScanner getScanner(String tableName) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + + /** + * 检索表中指定数据 + * + * @param tableName 表名 + * @param filterList 过滤器 + */ + + public static ResultScanner getScanner(String tableName, FilterList filterList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + scan.setFilter(filterList); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 检索表中指定数据 + * + * @param tableName 表名 + * @param startRowKey 起始 RowKey + * @param endRowKey 终止 RowKey + * @param filterList 过滤器 + */ + + public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey, + FilterList filterList) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Scan scan = new Scan(); + scan.withStartRow(Bytes.toBytes(startRowKey)); + scan.withStopRow(Bytes.toBytes(endRowKey)); + scan.setFilter(filterList); + return table.getScanner(scan); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 删除指定行记录 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + */ + public static boolean deleteRow(String tableName, String rowKey) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Delete delete = new Delete(Bytes.toBytes(rowKey)); + table.delete(delete); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + + + /** + * 删除指定行指定列 + * + * @param tableName 表名 + * @param rowKey 唯一标识 + * @param familyName 列族 + * @param qualifier 列标识 + */ + public static boolean deleteColumn(String tableName, String rowKey, String familyName, + String qualifier) { + try { + Table table = connection.getTable(TableName.valueOf(tableName)); + Delete delete = new Delete(Bytes.toBytes(rowKey)); + delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier)); + table.delete(delete); + table.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return true; + } + +} +``` + + + +## 四、正确连接Hbase + +在上面的代码中,在类加载时就初始化了 Connection 连接,并且之后的方法都是复用这个 Connection,这时我们可能会考虑是否可以使用自定义连接池来获取更好的性能表现?实际上这是没有必要的。 + +首先官方对于 `Connection` 的使用说明如下: + +```properties +Connection Pooling For applications which require high-end multithreaded +access (e.g., web-servers or application servers that may serve many +application threads in a single JVM), you can pre-create a Connection, +as shown in the following example: + +对于高并发多线程访问的应用程序(例如,在单个 JVM 中存在的为多个线程服务的 Web 服务器或应用程序服务器), +您只需要预先创建一个 Connection。例子如下: + +// Create a connection to the cluster. +Configuration conf = HBaseConfiguration.create(); +try (Connection connection = ConnectionFactory.createConnection(conf); + Table table = connection.getTable(TableName.valueOf(tablename))) { + // use table as needed, the table returned is lightweight +} +``` + +之所以能这样使用,这是因为 Connection 并不是一个简单的 socket 连接,[接口文档](https://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Connection.html) 中对 Connection 的表述是: + +```properties +A cluster connection encapsulating lower level individual connections to actual servers and a +connection to zookeeper. Connections are instantiated through the ConnectionFactory class. +The lifecycle of the connection is managed by the caller, who has to close() the connection +to release the resources. + +Connection 是一个集群连接,封装了与多台服务器(Matser/Region Server)的底层连接以及与 zookeeper 的连接。 +连接通过 ConnectionFactory 类实例化。连接的生命周期由调用者管理,调用者必须使用 close() 关闭连接以释放资源。 +``` + +之所以封装这些连接,是因为 HBase 客户端需要连接三个不同的服务角色: + ++ **Zookeeper** :主要用于获取 `meta` 表的位置信息,Master 的信息; ++ **HBase Master** :主要用于执行 HBaseAdmin 接口的一些操作,例如建表等; ++ **HBase RegionServer** :用于读、写数据。 + +
+ +Connection 对象和实际的 Socket 连接之间的对应关系如下图: + +
+ +> 上面两张图片引用自博客:[连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) + +在 HBase 客户端代码中,真正对应 Socket 连接的是 `RpcConnection` 对象。HBase 使用 `PoolMap` 这种数据结构来存储客户端到 HBase 服务器之间的连接。`PoolMap` 的内部有一个 `ConcurrentHashMap` 实例,其 key 是 `ConnectionId`(封装了服务器地址和用户 ticket),value 是一个 `RpcConnection` 对象的资源池。当 HBase 需要连接一个服务器时,首先会根据 `ConnectionId` 找到对应的连接池,然后从连接池中取出一个连接对象。 + +```java +@InterfaceAudience.Private +public class PoolMap implements Map { + private PoolType poolType; + + private int poolMaxSize; + + private Map> pools = new ConcurrentHashMap<>(); + + public PoolMap(PoolType poolType) { + this.poolType = poolType; + } + ..... +``` + +HBase 中提供了三种资源池的实现,分别是 `Reusable`,`RoundRobin` 和 `ThreadLocal`。具体实现可以通 `hbase.client.ipc.pool.type` 配置项指定,默认为 `Reusable`。连接池的大小也可以通过 `hbase.client.ipc.pool.size` 配置项指定,默认为 1,即每个 Server 1 个连接。也可以通过修改配置实现: + +```java +config.set("hbase.client.ipc.pool.type",...); +config.set("hbase.client.ipc.pool.size",...); +connection = ConnectionFactory.createConnection(config); +``` + +由此可以看出 HBase 中 Connection 类已经实现了对连接的管理功能,所以我们不必在 Connection 上在做额外的管理。 + +另外,Connection 是线程安全的,但 Table 和 Admin 却不是线程安全的,因此正确的做法是一个进程共用一个 Connection 对象,而在不同的线程中使用单独的 Table 和 Admin 对象。Table 和 Admin 的获取操作 `getTable()` 和 `getAdmin()` 都是轻量级,所以不必担心性能的消耗,同时建议在使用完成后显示的调用 `close()` 方法来关闭它们。 + + + +## 参考资料 + +1. [连接 HBase 的正确姿势](https://yq.aliyun.com/articles/581702?spm=a2c4e.11157919.spm-cont-list.1.146c27aeFxoMsN%20%E8%BF%9E%E6%8E%A5HBase%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%A7%BF%E5%8A%BF) +2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Shell.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Shell.md" new file mode 100644 index 0000000..d9417f2 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase_Shell.md" @@ -0,0 +1,279 @@ +# Hbase 常用 Shell 命令 + + + +## 一、基本命令 + +打开 Hbase Shell: + +```shell +# hbase shell +``` + +#### 1.1 获取帮助 + +```shell +# 获取帮助 +help +# 获取命令的详细信息 +help 'status' +``` + +#### 1.2 查看服务器状态 + +```shell +status +``` + +#### 1.3 查看版本信息 +```shell +version +``` + + + +## 二、关于表的操作 + + +#### 2.1 查看所有表 + +```shell +list +``` + +#### 2.2 创建表 + + **命令格式**: create '表名称', '列族名称 1','列族名称 2','列名称 N' + +```shell +# 创建一张名为Student的表,包含基本信息(baseInfo)、学校信息(schoolInfo)两个列族 +create 'Student','baseInfo','schoolInfo' +``` + +#### 2.3 查看表的基本信息 + + **命令格式**:desc '表名' + +```shell +describe 'Student' +``` + +#### 2.4 表的启用/禁用 + +enable 和 disable 可以启用/禁用这个表,is_enabled 和 is_disabled 来检查表是否被禁用 + +```shell +# 禁用表 +disable 'Student' +# 检查表是否被禁用 +is_disabled 'Student' +# 启用表 +enable 'Student' +# 检查表是否被启用 +is_enabled 'Student' +``` + +#### 2.5 检查表是否存在 + +```shell +exists 'Student' +``` + +#### 2.6 删除表 + +```shell +# 删除表前需要先禁用表 +disable 'Student' +# 删除表 +drop 'Student' +``` + + + +## 三、增删改 + + +#### 3.1 添加列族 + + **命令格式**: alter '表名', '列族名' + +```shell +alter 'Student', 'teacherInfo' +``` + +#### 3.2 删除列族 + + **命令格式**:alter '表名', {NAME => '列族名', METHOD => 'delete'} + +```shell +alter 'Student', {NAME => 'teacherInfo', METHOD => 'delete'} +``` + +#### 3.3 更改列族存储版本的限制 + +默认情况下,列族只存储一个版本的数据,如果需要存储多个版本的数据,则需要修改列族的属性。修改后可通过 `desc` 命令查看。 + +```shell +alter 'Student',{NAME=>'baseInfo',VERSIONS=>3} +``` + +#### 3.4 插入数据 + + **命令格式**:put '表名', '行键','列族:列','值' + +**注意:如果新增数据的行键值、列族名、列名与原有数据完全相同,则相当于更新操作** + +```shell +put 'Student', 'rowkey1','baseInfo:name','tom' +put 'Student', 'rowkey1','baseInfo:birthday','1990-01-09' +put 'Student', 'rowkey1','baseInfo:age','29' +put 'Student', 'rowkey1','schoolInfo:name','Havard' +put 'Student', 'rowkey1','schoolInfo:localtion','Boston' + +put 'Student', 'rowkey2','baseInfo:name','jack' +put 'Student', 'rowkey2','baseInfo:birthday','1998-08-22' +put 'Student', 'rowkey2','baseInfo:age','21' +put 'Student', 'rowkey2','schoolInfo:name','yale' +put 'Student', 'rowkey2','schoolInfo:localtion','New Haven' + +put 'Student', 'rowkey3','baseInfo:name','maike' +put 'Student', 'rowkey3','baseInfo:birthday','1995-01-22' +put 'Student', 'rowkey3','baseInfo:age','24' +put 'Student', 'rowkey3','schoolInfo:name','yale' +put 'Student', 'rowkey3','schoolInfo:localtion','New Haven' + +put 'Student', 'wrowkey4','baseInfo:name','maike-jack' +``` + +#### 3.5 获取指定行、指定行中的列族、列的信息 + +```shell +# 获取指定行中所有列的数据信息 +get 'Student','rowkey3' +# 获取指定行中指定列族下所有列的数据信息 +get 'Student','rowkey3','baseInfo' +# 获取指定行中指定列的数据信息 +get 'Student','rowkey3','baseInfo:name' +``` + +#### 3.6 删除指定行、指定行中的列 + +```shell +# 删除指定行 +delete 'Student','rowkey3' +# 删除指定行中指定列的数据 +delete 'Student','rowkey3','baseInfo:name' +``` + + + +## 四、查询 + +hbase 中访问数据有两种基本的方式: + ++ 按指定 rowkey 获取数据:get 方法; + ++ 按指定条件获取数据:scan 方法。 + +`scan` 可以设置 begin 和 end 参数来访问一个范围内所有的数据。get 本质上就是 begin 和 end 相等的一种特殊的 scan。 + +#### 4.1Get查询 + +```shell +# 获取指定行中所有列的数据信息 +get 'Student','rowkey3' +# 获取指定行中指定列族下所有列的数据信息 +get 'Student','rowkey3','baseInfo' +# 获取指定行中指定列的数据信息 +get 'Student','rowkey3','baseInfo:name' +``` + +#### 4.2 查询整表数据 + +```shell +scan 'Student' +``` + +#### 4.3 查询指定列簇的数据 + +```shell +scan 'Student', {COLUMN=>'baseInfo'} +``` + +#### 4.4 条件查询 + +```shell +# 查询指定列的数据 +scan 'Student', {COLUMNS=> 'baseInfo:birthday'} +``` + +除了列 `(COLUMNS)` 修饰词外,HBase 还支持 `Limit`(限制查询结果行数),`STARTROW`(`ROWKEY` 起始行,会先根据这个 `key` 定位到 `region`,再向后扫描)、`STOPROW`(结束行)、`TIMERANGE`(限定时间戳范围)、`VERSIONS`(版本数)、和 `FILTER`(按条件过滤行)等。 + +如下代表从 `rowkey2` 这个 `rowkey` 开始,查找下两个行的最新 3 个版本的 name 列的数据: + +```shell +scan 'Student', {COLUMNS=> 'baseInfo:name',STARTROW => 'rowkey2',STOPROW => 'wrowkey4',LIMIT=>2, VERSIONS=>3} +``` + +#### 4.5 条件过滤 + +Filter 可以设定一系列条件来进行过滤。如我们要查询值等于 24 的所有数据: + +```shell +scan 'Student', FILTER=>"ValueFilter(=,'binary:24')" +``` + +值包含 yale 的所有数据: + +```shell +scan 'Student', FILTER=>"ValueFilter(=,'substring:yale')" +``` + +列名中的前缀为 birth 的: + +```shell +scan 'Student', FILTER=>"ColumnPrefixFilter('birth')" +``` + +FILTER 中支持多个过滤条件通过括号、AND 和 OR 进行组合: + +```shell +# 列名中的前缀为birth且列值中包含1998的数据 +scan 'Student', FILTER=>"ColumnPrefixFilter('birth') AND ValueFilter ValueFilter(=,'substring:1998')" +``` + +`PrefixFilter` 用于对 Rowkey 的前缀进行判断: + +```shell +scan 'Student', FILTER=>"PrefixFilter('wr')" +``` + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\215\217\345\244\204\347\220\206\345\231\250\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\215\217\345\244\204\347\220\206\345\231\250\350\257\246\350\247\243.md" new file mode 100644 index 0000000..6949916 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\215\217\345\244\204\347\220\206\345\231\250\350\257\246\350\247\243.md" @@ -0,0 +1,490 @@ +# Hbase 协处理器 + + + + +## 一、简述 + +在使用 HBase 时,如果你的数据量达到了数十亿行或数百万列,此时能否在查询中返回大量数据将受制于网络的带宽,即便网络状况允许,但是客户端的计算处理也未必能够满足要求。在这种情况下,协处理器(Coprocessors)应运而生。它允许你将业务计算代码放入在 RegionServer 的协处理器中,将处理好的数据再返回给客户端,这可以极大地降低需要传输的数据量,从而获得性能上的提升。同时协处理器也允许用户扩展实现 HBase 目前所不具备的功能,如权限校验、二级索引、完整性约束等。 + + + +## 二、协处理器类型 + +### 2.1 Observer协处理器 + +#### 1. 功能 + +Observer 协处理器类似于关系型数据库中的触发器,当发生某些事件的时候这类协处理器会被 Server 端调用。通常可以用来实现下面功能: + ++ **权限校验**:在执行 `Get` 或 `Put` 操作之前,您可以使用 `preGet` 或 `prePut` 方法检查权限; ++ **完整性约束**: HBase 不支持关系型数据库中的外键功能,可以通过触发器在插入或者删除数据的时候,对关联的数据进行检查; ++ **二级索引**: 可以使用协处理器来维护二级索引。 + +
+ +#### 2. 类型 + +当前 Observer 协处理器有以下四种类型: + ++ **RegionObserver** : + 允许您观察 Region 上的事件,例如 Get 和 Put 操作。 ++ **RegionServerObserver** : + 允许您观察与 RegionServer 操作相关的事件,例如启动,停止或执行合并,提交或回滚。 ++ **MasterObserver** : + 允许您观察与 HBase Master 相关的事件,例如表创建,删除或 schema 修改。 ++ **WalObserver** : + 允许您观察与预写日志(WAL)相关的事件。 + +
+ +#### 3. 接口 + +以上四种类型的 Observer 协处理器均继承自 `Coprocessor` 接口,这四个接口中分别定义了所有可用的钩子方法,以便在对应方法前后执行特定的操作。通常情况下,我们并不会直接实现上面接口,而是继承其 Base 实现类,Base 实现类只是简单空实现了接口中的方法,这样我们在实现自定义的协处理器时,就不必实现所有方法,只需要重写必要方法即可。 + +
+ +这里以 `RegionObservers ` 为例,其接口类中定义了所有可用的钩子方法,下面截取了部分方法的定义,多数方法都是成对出现的,有 `pre` 就有 `post`: + +
+ +
+ +#### 4. 执行流程 + +
+ ++ 客户端发出 put 请求 ++ 该请求被分派给合适的 RegionServer 和 region ++ coprocessorHost 拦截该请求,然后在该表的每个 RegionObserver 上调用 prePut() ++ 如果没有被 `prePut()` 拦截,该请求继续送到 region,然后进行处理 ++ region 产生的结果再次被 CoprocessorHost 拦截,调用 `postPut()` ++ 假如没有 `postPut()` 拦截该响应,最终结果被返回给客户端 + +如果大家了解 Spring,可以将这种执行方式类比于其 AOP 的执行原理即可,官方文档当中也是这样类比的: + +>If you are familiar with Aspect Oriented Programming (AOP), you can think of a coprocessor as applying advice by intercepting a request and then running some custom code,before passing the request on to its final destination (or even changing the destination). +> +>如果您熟悉面向切面编程(AOP),您可以将协处理器视为通过拦截请求然后运行一些自定义代码来使用 Advice,然后将请求传递到其最终目标(或者更改目标)。 + + + +### 2.2 Endpoint协处理器 + +Endpoint 协处理器类似于关系型数据库中的存储过程。客户端可以调用 Endpoint 协处理器在服务端对数据进行处理,然后再返回。 + +以聚集操作为例,如果没有协处理器,当用户需要找出一张表中的最大数据,即 max 聚合操作,就必须进行全表扫描,然后在客户端上遍历扫描结果,这必然会加重了客户端处理数据的压力。利用 Coprocessor,用户可以将求最大值的代码部署到 HBase Server 端,HBase 将利用底层 cluster 的多个节点并发执行求最大值的操作。即在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出来,仅仅将该 max 值返回给客户端。之后客户端只需要将每个 Region 的最大值进行比较而找到其中最大的值即可。 + + + +## 三、协处理的加载方式 + +要使用我们自己开发的协处理器,必须通过静态(使用 HBase 配置)或动态(使用 HBase Shell 或 Java API)加载它。 + ++ 静态加载的协处理器称之为 **System Coprocessor**(系统级协处理器),作用范围是整个 HBase 上的所有表,需要重启 HBase 服务; ++ 动态加载的协处理器称之为 **Table Coprocessor**(表处理器),作用于指定的表,不需要重启 HBase 服务。 + +其加载和卸载方式分别介绍如下。 + + + +## 四、静态加载与卸载 + +### 4.1 静态加载 + +静态加载分以下三步: + +1. 在 `hbase-site.xml` 定义需要加载的协处理器。 + +```xml + + hbase.coprocessor.region.classes + org.myname.hbase.coprocessor.endpoint.SumEndPoint + +``` + + ` ` 标签的值必须是下面其中之一: + + + RegionObservers 和 Endpoints 协处理器:`hbase.coprocessor.region.classes` + + WALObservers 协处理器: `hbase.coprocessor.wal.classes` + + MasterObservers 协处理器:`hbase.coprocessor.master.classes` + + `` 必须是协处理器实现类的全限定类名。如果为加载指定了多个类,则类名必须以逗号分隔。 + +2. 将 jar(包含代码和所有依赖项) 放入 HBase 安装目录中的 `lib` 目录下; + +3. 重启 HBase。 + +
+ +### 4.2 静态卸载 + +1. 从 hbase-site.xml 中删除配置的协处理器的\元素及其子元素; + +2. 从类路径或 HBase 的 lib 目录中删除协处理器的 JAR 文件(可选); + +3. 重启 HBase。 + + + + +## 五、动态加载与卸载 + +使用动态加载协处理器,不需要重新启动 HBase。但动态加载的协处理器是基于每个表加载的,只能用于所指定的表。 +此外,在使用动态加载必须使表脱机(disable)以加载协处理器。动态加载通常有两种方式:Shell 和 Java API 。 + +> 以下示例基于两个前提: +> +> 1. coprocessor.jar 包含协处理器实现及其所有依赖项。 +> 2. JAR 包存放在 HDFS 上的路径为:hdfs:// \:\ / user / \ /coprocessor.jar + +### 5.1 HBase Shell动态加载 + +1. 使用 HBase Shell 禁用表 + +```shell +hbase > disable 'tableName' +``` + +2. 使用如下命令加载协处理器 + +```shell +hbase > alter 'tableName', METHOD => 'table_att', 'Coprocessor'=>'hdfs://:/ +user//coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverExample|1073741823| +arg1=1,arg2=2' +``` + +`Coprocessor` 包含由管道(|)字符分隔的四个参数,按顺序解释如下: + ++ **JAR 包路径**:通常为 JAR 包在 HDFS 上的路径。关于路径以下两点需要注意: ++ 允许使用通配符,例如:`hdfs://:/user//*.jar` 来添加指定的 JAR 包; + ++ 可以使指定目录,例如:`hdfs://:/user//` ,这会添加目录中的所有 JAR 包,但不会搜索子目录中的 JAR 包。 + ++ **类名**:协处理器的完整类名。 ++ **优先级**:协处理器的优先级,遵循数字的自然序,即值越小优先级越高。可以为空,在这种情况下,将分配默认优先级值。 ++ **可选参数** :传递的协处理器的可选参数。 + +3. 启用表 + +```shell +hbase > enable 'tableName' +``` + +4. 验证协处理器是否已加载 + +```shell +hbase > describe 'tableName' +``` + +协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功。 + +
+ +### 5.2 HBase Shell动态卸载 + +1. 禁用表 + + ```shell +hbase> disable 'tableName' + ``` + +2. 移除表协处理器 + +```shell +hbase> alter 'tableName', METHOD => 'table_att_unset', NAME => 'coprocessor$1' +``` + +3. 启用表 + +```shell +hbase> enable 'tableName' +``` + +
+ +### 5.3 Java API 动态加载 + +```java +TableName tableName = TableName.valueOf("users"); +String path = "hdfs://:/user//coprocessor.jar"; +Configuration conf = HBaseConfiguration.create(); +Connection connection = ConnectionFactory.createConnection(conf); +Admin admin = connection.getAdmin(); +admin.disableTable(tableName); +HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName); +HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet"); +columnFamily1.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily1); +HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet"); +columnFamily2.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily2); +hTableDescriptor.setValue("COPROCESSOR$1", path + "|" ++ RegionObserverExample.class.getCanonicalName() + "|" ++ Coprocessor.PRIORITY_USER); +admin.modifyTable(tableName, hTableDescriptor); +admin.enableTable(tableName); +``` + +在 HBase 0.96 及其以后版本中,HTableDescriptor 的 addCoprocessor() 方法提供了一种更为简便的加载方法。 + +```java +TableName tableName = TableName.valueOf("users"); +Path path = new Path("hdfs://:/user//coprocessor.jar"); +Configuration conf = HBaseConfiguration.create(); +Connection connection = ConnectionFactory.createConnection(conf); +Admin admin = connection.getAdmin(); +admin.disableTable(tableName); +HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName); +HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet"); +columnFamily1.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily1); +HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet"); +columnFamily2.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily2); +hTableDescriptor.addCoprocessor(RegionObserverExample.class.getCanonicalName(), path, +Coprocessor.PRIORITY_USER, null); +admin.modifyTable(tableName, hTableDescriptor); +admin.enableTable(tableName); +``` + + + +### 5.4 Java API 动态卸载 + +卸载其实就是重新定义表但不设置协处理器。这会删除所有表上的协处理器。 + +```java +TableName tableName = TableName.valueOf("users"); +String path = "hdfs://:/user//coprocessor.jar"; +Configuration conf = HBaseConfiguration.create(); +Connection connection = ConnectionFactory.createConnection(conf); +Admin admin = connection.getAdmin(); +admin.disableTable(tableName); +HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName); +HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet"); +columnFamily1.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily1); +HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet"); +columnFamily2.setMaxVersions(3); +hTableDescriptor.addFamily(columnFamily2); +admin.modifyTable(tableName, hTableDescriptor); +admin.enableTable(tableName); +``` + + + +## 六、协处理器案例 + +这里给出一个简单的案例,实现一个类似于 Redis 中 `append` 命令的协处理器,当我们对已有列执行 put 操作时候,HBase 默认执行的是 update 操作,这里我们修改为执行 append 操作。 + +```shell +# redis append 命令示例 +redis> EXISTS mykey +(integer) 0 +redis> APPEND mykey "Hello" +(integer) 5 +redis> APPEND mykey " World" +(integer) 11 +redis> GET mykey +"Hello World" +``` + +### 6.1 创建测试表 + +```shell +# 创建一张杂志表 有文章和图片两个列族 +hbase > create 'magazine','article','picture' +``` + +### 6.2 协处理器编程 + +> 完整代码可见本仓库:[hbase-observer-coprocessor](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hbase\hbase-observer-coprocessor) + +新建 Maven 工程,导入下面依赖: + +```xml + + org.apache.hbase + hbase-common + 1.2.0 + + + org.apache.hbase + hbase-server + 1.2.0 + +``` + +继承 `BaseRegionObserver` 实现我们自定义的 `RegionObserver`,对相同的 `article:content` 执行 put 命令时,将新插入的内容添加到原有内容的末尾,代码如下: + +```java +public class AppendRegionObserver extends BaseRegionObserver { + + private byte[] columnFamily = Bytes.toBytes("article"); + private byte[] qualifier = Bytes.toBytes("content"); + + @Override + public void prePut(ObserverContext e, Put put, WALEdit edit, + Durability durability) throws IOException { + if (put.has(columnFamily, qualifier)) { + // 遍历查询结果,获取指定列的原值 + Result rs = e.getEnvironment().getRegion().get(new Get(put.getRow())); + String oldValue = ""; + for (Cell cell : rs.rawCells()) + if (CellUtil.matchingColumn(cell, columnFamily, qualifier)) { + oldValue = Bytes.toString(CellUtil.cloneValue(cell)); + } + + // 获取指定列新插入的值 + List cells = put.get(columnFamily, qualifier); + String newValue = ""; + for (Cell cell : cells) { + if (CellUtil.matchingColumn(cell, columnFamily, qualifier)) { + newValue = Bytes.toString(CellUtil.cloneValue(cell)); + } + } + + // Append 操作 + put.addColumn(columnFamily, qualifier, Bytes.toBytes(oldValue + newValue)); + } + } +} +``` + +### 6.3 打包项目 + +使用 maven 命令进行打包,打包后的文件名为 `hbase-observer-coprocessor-1.0-SNAPSHOT.jar` + +```shell +# mvn clean package +``` + +### 6.4 上传JAR包到HDFS + +```shell +# 上传项目到HDFS上的hbase目录 +hadoop fs -put /usr/app/hbase-observer-coprocessor-1.0-SNAPSHOT.jar /hbase +# 查看上传是否成功 +hadoop fs -ls /hbase +``` + +
+ +### 6.5 加载协处理器 + +1. 加载协处理器前需要先禁用表 + +```shell +hbase > disable 'magazine' +``` +2. 加载协处理器 + +```shell +hbase > alter 'magazine', METHOD => 'table_att', 'Coprocessor'=>'hdfs://hadoop001:8020/hbase/hbase-observer-coprocessor-1.0-SNAPSHOT.jar|com.heibaiying.AppendRegionObserver|1001|' +``` + +3. 启用表 + +```shell +hbase > enable 'magazine' +``` + +4. 查看协处理器是否加载成功 + +```shell +hbase > desc 'magazine' +``` + +协处理器出现在 `TABLE_ATTRIBUTES` 属性中则代表加载成功,如下图: + +
+ +### 6.6 测试加载结果 + +插入一组测试数据: + +```shell +hbase > put 'magazine', 'rowkey1','article:content','Hello' +hbase > get 'magazine','rowkey1','article:content' +hbase > put 'magazine', 'rowkey1','article:content','World' +hbase > get 'magazine','rowkey1','article:content' +``` + +可以看到对于指定列的值已经执行了 append 操作: + +
+ +插入一组对照数据: + +```shell +hbase > put 'magazine', 'rowkey1','article:author','zhangsan' +hbase > get 'magazine','rowkey1','article:author' +hbase > put 'magazine', 'rowkey1','article:author','lisi' +hbase > get 'magazine','rowkey1','article:author' +``` + +可以看到对于正常的列还是执行 update 操作: + +
+ +### 6.7 卸载协处理器 +1. 卸载协处理器前需要先禁用表 + +```shell +hbase > disable 'magazine' +``` +2. 卸载协处理器 + +```shell +hbase > alter 'magazine', METHOD => 'table_att_unset', NAME => 'coprocessor$1' +``` + +3. 启用表 + +```shell +hbase > enable 'magazine' +``` + +4. 查看协处理器是否卸载成功 + +```shell +hbase > desc 'magazine' +``` + +
+ +### 6.8 测试卸载结果 + +依次执行下面命令可以测试卸载是否成功 + +```shell +hbase > get 'magazine','rowkey1','article:content' +hbase > put 'magazine', 'rowkey1','article:content','Hello' +hbase > get 'magazine','rowkey1','article:content' +``` + +
+ + + +## 参考资料 + +1. [Apache HBase Coprocessors](http://hbase.apache.org/book.html#cp) +2. [Apache HBase Coprocessor Introduction](https://blogs.apache.org/hbase/entry/coprocessor_introduction) +3. [HBase 高階知識](https://www.itread01.com/content/1546245908.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\256\271\347\201\276\344\270\216\345\244\207\344\273\275.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\256\271\347\201\276\344\270\216\345\244\207\344\273\275.md" new file mode 100644 index 0000000..0dfb055 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\345\256\271\347\201\276\344\270\216\345\244\207\344\273\275.md" @@ -0,0 +1,196 @@ +# Hbase容灾与备份 + + + +## 一、前言 + +本文主要介绍 Hbase 常用的三种简单的容灾备份方案,即**CopyTable**、**Export**/**Import**、**Snapshot**。分别介绍如下: + + + +## 二、CopyTable + +### 2.1 简介 + +**CopyTable**可以将现有表的数据复制到新表中,具有以下特点: + +- 支持时间区间 、row 区间 、改变表名称 、改变列族名称 、以及是否 Copy 已被删除的数据等功能; +- 执行命令前,需先创建与原表结构相同的新表; +- `CopyTable` 的操作是基于 HBase Client API 进行的,即采用 `scan` 进行查询, 采用 `put` 进行写入。 + +### 2.2 命令格式 + +```shell +Usage: CopyTable [general options] [--starttime=X] [--endtime=Y] [--new.name=NEW] [--peer.adr=ADR] +``` + +### 2.3 常用命令 + +1. 同集群下 CopyTable + +```shell +hbase org.apache.hadoop.hbase.mapreduce.CopyTable --new.name=tableCopy tableOrig +``` + +2. 不同集群下 CopyTable + +```shell +# 两表名称相同的情况 +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--peer.adr=dstClusterZK:2181:/hbase tableOrig + +# 也可以指新的表名 +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--peer.adr=dstClusterZK:2181:/hbase \ +--new.name=tableCopy tableOrig +``` + + +3. 下面是一个官方给的比较完整的例子,指定开始和结束时间,集群地址,以及只复制指定的列族: + +```shell +hbase org.apache.hadoop.hbase.mapreduce.CopyTable \ +--starttime=1265875194289 \ +--endtime=1265878794289 \ +--peer.adr=server1,server2,server3:2181:/hbase \ +--families=myOldCf:myNewCf,cf2,cf3 TestTable +``` + +### 2.4 更多参数 + +可以通过 `--help` 查看更多支持的参数 + +```shell +# hbase org.apache.hadoop.hbase.mapreduce.CopyTable --help +``` + +
+ + + +## 三、Export/Import + +### 3.1 简介 + +- `Export` 支持导出数据到 HDFS, `Import` 支持从 HDFS 导入数据。`Export` 还支持指定导出数据的开始时间和结束时间,因此可以用于增量备份。 +- `Export` 导出与 `CopyTable` 一样,依赖 HBase 的 `scan` 操作 + +### 3.2 命令格式 + +```shell +# Export +hbase org.apache.hadoop.hbase.mapreduce.Export [ [ []]] + +# Inport +hbase org.apache.hadoop.hbase.mapreduce.Import +``` + ++ 导出的 `outputdir` 目录可以不用预先创建,程序会自动创建。导出完成后,导出文件的所有权将由执行导出命令的用户所拥有。 ++ 默认情况下,仅导出给定 `Cell` 的最新版本,而不管历史版本。要导出多个版本,需要将 `` 参数替换为所需的版本数。 + +### 3.3 常用命令 + +1. 导出命令 + +```shell +hbase org.apache.hadoop.hbase.mapreduce.Export tableName hdfs 路径/tableName.db +``` + +2. 导入命令 + +``` +hbase org.apache.hadoop.hbase.mapreduce.Import tableName hdfs 路径/tableName.db +``` + + + +## 四、Snapshot + +### 4.1 简介 + +HBase 的快照 (Snapshot) 功能允许您获取表的副本 (包括内容和元数据),并且性能开销很小。因为快照存储的仅仅是表的元数据和 HFiles 的信息。快照的 `clone` 操作会从该快照创建新表,快照的 `restore` 操作会将表的内容还原到快照节点。`clone` 和 `restore` 操作不需要复制任何数据,因为底层 HFiles(包含 HBase 表数据的文件) 不会被修改,修改的只是表的元数据信息。 + +### 4.2 配置 + +HBase 快照功能默认没有开启,如果要开启快照,需要在 `hbase-site.xml` 文件中添加如下配置项: + +```xml + + hbase.snapshot.enabled + true + +``` + + + +### 4.3 常用命令 + +快照的所有命令都需要在 Hbase Shell 交互式命令行中执行。 + +#### 1. Take a Snapshot + +```shell +# 拍摄快照 +hbase> snapshot '表名', '快照名' +``` + +默认情况下拍摄快照之前会在内存中执行数据刷新。以保证内存中的数据包含在快照中。但是如果你不希望包含内存中的数据,则可以使用 `SKIP_FLUSH` 选项禁止刷新。 + +```shell +# 禁止内存刷新 +hbase> snapshot '表名', '快照名', {SKIP_FLUSH => true} +``` + +#### 2. Listing Snapshots + +```shell +# 获取快照列表 +hbase> list_snapshots +``` + +#### 3. Deleting Snapshots + +```shell +# 删除快照 +hbase> delete_snapshot '快照名' +``` + +#### 4. Clone a table from snapshot + +```shell +# 从现有的快照创建一张新表 +hbase> clone_snapshot '快照名', '新表名' +``` + +#### 5. Restore a snapshot + +将表恢复到快照节点,恢复操作需要先禁用表 + +```shell +hbase> disable '表名' +hbase> restore_snapshot '快照名' +``` + +这里需要注意的是:是如果 HBase 配置了基于 Replication 的主从复制,由于 Replication 在日志级别工作,而快照在文件系统级别工作,因此在还原之后,会出现副本与主服务器处于不同的状态的情况。这时候可以先停止同步,所有服务器还原到一致的数据点后再重新建立同步。 + + + +## 参考资料 + +1. [Online Apache HBase Backups with CopyTable](https://blog.cloudera.com/blog/2012/06/online-hbase-backups-with-copytable-2/) +2. [Apache HBase ™ Reference Guide](http://hbase.apache.org/book.htm) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\232\204SQL\344\270\255\351\227\264\345\261\202_Phoenix.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\232\204SQL\344\270\255\351\227\264\345\261\202_Phoenix.md" new file mode 100644 index 0000000..e89c512 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\232\204SQL\344\270\255\351\227\264\345\261\202_Phoenix.md" @@ -0,0 +1,241 @@ +# Hbase的SQL中间层——Phoenix + + + +## 一、Phoenix简介 + +`Phoenix` 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前,如果你要访问 HBase,只能调用它的 Java API,但相比于使用一行 SQL 就能实现数据查询,HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`,即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。 + +其次 `Phoenix` 的性能表现也非常优异,`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan,通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性,因为以上的优点,所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。 + +
+ + +## 二、Phoenix安装 + +> 我们可以按照官方安装说明进行安装,官方说明如下: +> +> - download and expand our installation tar +> - copy the phoenix server jar that is compatible with your HBase installation into the lib directory of every region server +> - restart the region servers +> - add the phoenix client jar to the classpath of your HBase client +> - download and setup SQuirrel as your SQL client so you can issue adhoc SQL against your HBase cluster + +### 2.1 下载并解压 + +官方针对 Apache 版本和 CDH 版本的 HBase 均提供了安装包,按需下载即可。官方下载地址: http://phoenix.apache.org/download.html + +```shell +# 下载 +wget http://mirror.bit.edu.cn/apache/phoenix/apache-phoenix-4.14.0-cdh5.14.2/bin/apache-phoenix-4.14.0-cdh5.14.2-bin.tar.gz +# 解压 +tar tar apache-phoenix-4.14.0-cdh5.14.2-bin.tar.gz +``` + +### 2.2 拷贝Jar包 + +按照官方文档的说明,需要将 `phoenix server jar` 添加到所有 `Region Servers` 的安装目录的 `lib` 目录下。 + +这里由于我搭建的是 HBase 伪集群,所以只需要拷贝到当前机器的 HBase 的 lib 目录下。如果是真实集群,则使用 scp 命令分发到所有 `Region Servers` 机器上。 + +```shell +cp /usr/app/apache-phoenix-4.14.0-cdh5.14.2-bin/phoenix-4.14.0-cdh5.14.2-server.jar /usr/app/hbase-1.2.0-cdh5.15.2/lib +``` + +### 2.3 重启 Region Servers + +```shell +# 停止Hbase +stop-hbase.sh +# 启动Hbase +start-hbase.sh +``` + +### 2.4 启动Phoenix + +在 Phoenix 解压目录下的 `bin` 目录下执行如下命令,需要指定 Zookeeper 的地址: + ++ 如果 HBase 采用 Standalone 模式或者伪集群模式搭建,则默认采用内置的 Zookeeper 服务,端口为 2181; ++ 如果是 HBase 是集群模式并采用外置的 Zookeeper 集群,则按照自己的实际情况进行指定。 + +```shell +# ./sqlline.py hadoop001:2181 +``` + +### 2.5 启动结果 + +启动后则进入了 Phoenix 交互式 SQL 命令行,可以使用 `!table` 或 `!tables` 查看当前所有表的信息 + +
+ + +## 三、Phoenix 简单使用 + +### 3.1 创建表 + +```sql +CREATE TABLE IF NOT EXISTS us_population ( + state CHAR(2) NOT NULL, + city VARCHAR NOT NULL, + population BIGINT + CONSTRAINT my_pk PRIMARY KEY (state, city)); +``` + +
+新建的表会按照特定的规则转换为 HBase 上的表,关于表的信息,可以通过 Hbase Web UI 进行查看: + +
+### 3.2 插入数据 + +Phoenix 中插入数据采用的是 `UPSERT` 而不是 `INSERT`,因为 Phoenix 并没有更新操作,插入相同主键的数据就视为更新,所以 `UPSERT` 就相当于 `UPDATE`+`INSERT` + +```shell +UPSERT INTO us_population VALUES('NY','New York',8143197); +UPSERT INTO us_population VALUES('CA','Los Angeles',3844829); +UPSERT INTO us_population VALUES('IL','Chicago',2842518); +UPSERT INTO us_population VALUES('TX','Houston',2016582); +UPSERT INTO us_population VALUES('PA','Philadelphia',1463281); +UPSERT INTO us_population VALUES('AZ','Phoenix',1461575); +UPSERT INTO us_population VALUES('TX','San Antonio',1256509); +UPSERT INTO us_population VALUES('CA','San Diego',1255540); +UPSERT INTO us_population VALUES('TX','Dallas',1213825); +UPSERT INTO us_population VALUES('CA','San Jose',912332); +``` + +### 3.3 修改数据 + +```sql +-- 插入主键相同的数据就视为更新 +UPSERT INTO us_population VALUES('NY','New York',999999); +``` + +
+### 3.4 删除数据 + +```sql +DELETE FROM us_population WHERE city='Dallas'; +``` + +
+### 3.5 查询数据 + +```sql +SELECT state as "州",count(city) as "市",sum(population) as "热度" +FROM us_population +GROUP BY state +ORDER BY sum(population) DESC; +``` + +
+ + +### 3.6 退出命令 + +```sql +!quit +``` + + + +### 3.7 扩展 + +从上面的操作中可以看出,Phoenix 支持大多数标准的 SQL 语法。关于 Phoenix 支持的语法、数据类型、函数、序列等详细信息,因为涉及内容很多,可以参考其官方文档,官方文档上有详细的说明: + ++ **语法 (Grammar)** :https://phoenix.apache.org/language/index.html + ++ **函数 (Functions)** :http://phoenix.apache.org/language/functions.html + ++ **数据类型 (Datatypes)** :http://phoenix.apache.org/language/datatypes.html + ++ **序列 (Sequences)** :http://phoenix.apache.org/sequences.html + ++ **联结查询 (Joins)** :http://phoenix.apache.org/joins.html + + + +## 四、Phoenix Java API + +因为 Phoenix 遵循 JDBC 规范,并提供了对应的数据库驱动 `PhoenixDriver`,这使得采用 Java 语言对其进行操作的时候,就如同对其他关系型数据库一样,下面给出基本的使用示例。 + +### 4.1 引入Phoenix core JAR包 + +如果是 maven 项目,直接在 maven 中央仓库找到对应的版本,导入依赖即可: + +```xml + + + org.apache.phoenix + phoenix-core + 4.14.0-cdh5.14.2 + +``` + +如果是普通项目,则可以从 Phoenix 解压目录下找到对应的 JAR 包,然后手动引入: + +
+### 4.2 简单的Java API实例 + +```java +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + + +public class PhoenixJavaApi { + + public static void main(String[] args) throws Exception { + + // 加载数据库驱动 + Class.forName("org.apache.phoenix.jdbc.PhoenixDriver"); + + /* + * 指定数据库地址,格式为 jdbc:phoenix:Zookeeper 地址 + * 如果 HBase 采用 Standalone 模式或者伪集群模式搭建,则 HBase 默认使用内置的 Zookeeper,默认端口为 2181 + */ + Connection connection = DriverManager.getConnection("jdbc:phoenix:192.168.200.226:2181"); + + PreparedStatement statement = connection.prepareStatement("SELECT * FROM us_population"); + + ResultSet resultSet = statement.executeQuery(); + + while (resultSet.next()) { + System.out.println(resultSet.getString("city") + " " + + resultSet.getInt("population")); + } + + statement.close(); + connection.close(); + } +} +``` + +结果如下: + +
+ + +实际的开发中我们通常都是采用第三方框架来操作数据库,如 `mybatis`,`Hibernate`,`Spring Data` 等。关于 Phoenix 与这些框架的整合步骤参见下一篇文章:[Spring/Spring Boot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spring+Mybtais+Phoenix整合.md) + +# 参考资料 + +1. http://phoenix.apache.org/ diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\256\200\344\273\213.md" new file mode 100644 index 0000000..436e4a0 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\256\200\344\273\213.md" @@ -0,0 +1,88 @@ +# HBase简介 + + + +## 一、Hadoop的局限 + +HBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。 + +
+ +要想明白为什么产生 HBase,就需要先了解一下 Hadoop 存在的限制?Hadoop 可以通过 HDFS 来存储结构化、半结构甚至非结构化的数据,它是传统数据库的补充,是海量数据存储的最佳方法,它针对大文件的存储,批量访问和流式访问都做了优化,同时也通过多副本解决了容灾问题。 + +但是 Hadoop 的缺陷在于它只能执行批处理,并且只能以顺序方式访问数据,这意味着即使是最简单的工作,也必须搜索整个数据集,无法实现对数据的随机访问。实现数据的随机访问是传统的关系型数据库所擅长的,但它们却不能用于海量数据的存储。在这种情况下,必须有一种新的方案来解决海量数据存储和随机访问的问题,HBase 就是其中之一 (HBase,Cassandra,couchDB,Dynamo 和 MongoDB 都能存储海量数据并支持随机访问)。 + +> 注:数据结构分类: +> +> - 结构化数据:即以关系型数据库表形式管理的数据; +> - 半结构化数据:非关系模型的,有基本固定结构模式的数据,例如日志文件、XML 文档、JSON 文档、Email 等; +> - 非结构化数据:没有固定模式的数据,如 WORD、PDF、PPT、EXL,各种格式的图片、视频等。 + + + +## 二、HBase简介 + +HBase 是一个构建在 Hadoop 文件系统之上的面向列的数据库管理系统。 + +HBase 是一种类似于 `Google’s Big Table` 的数据模型,它是 Hadoop 生态系统的一部分,它将数据存储在 HDFS 上,客户端可以通过 HBase 实现对 HDFS 上数据的随机访问。它具有以下特性: + ++ 不支持复杂的事务,只支持行级事务,即单行数据的读写都是原子性的; ++ 由于是采用 HDFS 作为底层存储,所以和 HDFS 一样,支持结构化、半结构化和非结构化的存储; ++ 支持通过增加机器进行横向扩展; ++ 支持数据分片; ++ 支持 RegionServers 之间的自动故障转移; ++ 易于使用的 Java 客户端 API; ++ 支持 BlockCache 和布隆过滤器; ++ 过滤器支持谓词下推。 + + + +## 三、HBase Table + +HBase 是一个面向 ` 列 ` 的数据库管理系统,这里更为确切的而说,HBase 是一个面向 ` 列族 ` 的数据库管理系统。表 schema 仅定义列族,表具有多个列族,每个列族可以包含任意数量的列,列由多个单元格(cell )组成,单元格可以存储多个版本的数据,多个版本数据以时间戳进行区分。 + +下图为 HBase 中一张表的: + ++ RowKey 为行的唯一标识,所有行按照 RowKey 的字典序进行排序; ++ 该表具有两个列族,分别是 personal 和 office; ++ 其中列族 personal 拥有 name、city、phone 三个列,列族 office 拥有 tel、addres 两个列。 + +
+ +> *图片引用自 : HBase 是列式存储数据库吗* *https://www.iteblog.com/archives/2498.html* + +Hbase 的表具有以下特点: + +- 容量大:一个表可以有数十亿行,上百万列; + +- 面向列:数据是按照列存储,每一列都单独存放,数据即索引,在查询时可以只访问指定列的数据,有效地降低了系统的 I/O 负担; + +- 稀疏性:空 (null) 列并不占用存储空间,表可以设计的非常稀疏 ; + +- 数据多版本:每个单元中的数据可以有多个版本,按照时间戳排序,新的数据在最上面; + +- 存储类型:所有数据的底层存储格式都是字节数组 (byte[])。 + + + +## 四、Phoenix + +`Phoenix` 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据。在 `Phoenix` 之前,如果你要访问 HBase,只能调用它的 Java API,但相比于使用一行 SQL 就能实现数据查询,HBase 的 API 还是过于复杂。`Phoenix` 的理念是 `we put sql SQL back in NOSQL`,即你可以使用标准的 SQL 就能完成对 HBase 上数据的操作。同时这也意味着你可以通过集成 `Spring Data JPA` 或 `Mybatis` 等常用的持久层框架来操作 HBase。 + +其次 `Phoenix` 的性能表现也非常优异,`Phoenix` 查询引擎会将 SQL 查询转换为一个或多个 HBase Scan,通过并行执行来生成标准的 JDBC 结果集。它通过直接使用 HBase API 以及协处理器和自定义过滤器,可以为小型数据查询提供毫秒级的性能,为千万行数据的查询提供秒级的性能。同时 Phoenix 还拥有二级索引等 HBase 不具备的特性,因为以上的优点,所以 `Phoenix` 成为了 HBase 最优秀的 SQL 中间层。 + + + + + +## 参考资料 + +1. [HBase - Overview](https://www.tutorialspoint.com/hbase/hbase_overview.htm) + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\263\273\347\273\237\346\236\266\346\236\204\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\263\273\347\273\237\346\236\266\346\236\204\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 0000000..1b871bd --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\347\263\273\347\273\237\346\236\266\346\236\204\345\217\212\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,222 @@ +# Hbase系统架构及数据结构 + + + +## 一、基本概念 + +一个典型的 Hbase Table 表如下: + +
+ +### 1.1 Row Key (行键) + +`Row Key` 是用来检索记录的主键。想要访问 HBase Table 中的数据,只有以下三种方式: + ++ 通过指定的 `Row Key` 进行访问; + ++ 通过 Row Key 的 range 进行访问,即访问指定范围内的行; + ++ 进行全表扫描。 + +`Row Key` 可以是任意字符串,存储时数据按照 `Row Key` 的字典序进行排序。这里需要注意以下两点: + ++ 因为字典序对 Int 排序的结果是 1,10,100,11,12,13,14,15,16,17,18,19,2,20,21,…,9,91,92,93,94,95,96,97,98,99。如果你使用整型的字符串作为行键,那么为了保持整型的自然序,行键必须用 0 作左填充。 + ++ 行的一次读写操作时原子性的 (不论一次读写多少列)。 + + + +### 1.2 Column Family(列族) + +HBase 表中的每个列,都归属于某个列族。列族是表的 Schema 的一部分,所以列族需要在创建表时进行定义。列族的所有列都以列族名作为前缀,例如 `courses:history`,`courses:math` 都属于 `courses` 这个列族。 + + + +### 1.3 Column Qualifier (列限定符) + +列限定符,你可以理解为是具体的列名,例如 `courses:history`,`courses:math` 都属于 `courses` 这个列族,它们的列限定符分别是 `history` 和 `math`。需要注意的是列限定符不是表 Schema 的一部分,你可以在插入数据的过程中动态创建列。 + + + +### 1.4 Column(列) + +HBase 中的列由列族和列限定符组成,它们由 `:`(冒号) 进行分隔,即一个完整的列名应该表述为 ` 列族名 :列限定符 `。 + + + +### 1.5 Cell + +`Cell` 是行,列族和列限定符的组合,并包含值和时间戳。你可以等价理解为关系型数据库中由指定行和指定列确定的一个单元格,但不同的是 HBase 中的一个单元格是由多个版本的数据组成的,每个版本的数据用时间戳进行区分。 + + + +### 1.6 Timestamp(时间戳) + +HBase 中通过 `row key` 和 `column` 确定的为一个存储单元称为 `Cell`。每个 `Cell` 都保存着同一份数据的多个版本。版本通过时间戳来索引,时间戳的类型是 64 位整型,时间戳可以由 HBase 在数据写入时自动赋值,也可以由客户显式指定。每个 `Cell` 中,不同版本的数据按照时间戳倒序排列,即最新的数据排在最前面。 + + + +## 二、存储结构 + +### 2.1 Regions + +HBase Table 中的所有行按照 `Row Key` 的字典序排列。HBase Tables 通过行键的范围 (row key range) 被水平切分成多个 `Region`, 一个 `Region` 包含了在 start key 和 end key 之间的所有行。 + +
+ +每个表一开始只有一个 `Region`,随着数据不断增加,`Region` 会不断增大,当增大到一个阀值的时候,`Region` 就会等分为两个新的 `Region`。当 Table 中的行不断增多,就会有越来越多的 `Region`。 + +
+ +`Region` 是 HBase 中**分布式存储和负载均衡的最小单元**。这意味着不同的 `Region` 可以分布在不同的 `Region Server` 上。但一个 `Region` 是不会拆分到多个 Server 上的。 + +
+ +### 2.2 Region Server + +`Region Server` 运行在 HDFS 的 DataNode 上。它具有以下组件: + +- **WAL(Write Ahead Log,预写日志)**:用于存储尚未进持久化存储的数据记录,以便在发生故障时进行恢复。 +- **BlockCache**:读缓存。它将频繁读取的数据存储在内存中,如果存储不足,它将按照 ` 最近最少使用原则 ` 清除多余的数据。 +- **MemStore**:写缓存。它存储尚未写入磁盘的新数据,并会在数据写入磁盘之前对其进行排序。每个 Region 上的每个列族都有一个 MemStore。 +- **HFile** :将行数据按照 Key\Values 的形式存储在文件系统上。 + +
+ + + +Region Server 存取一个子表时,会创建一个 Region 对象,然后对表的每个列族创建一个 `Store` 实例,每个 `Store` 会有 0 个或多个 `StoreFile` 与之对应,每个 `StoreFile` 则对应一个 `HFile`,HFile 就是实际存储在 HDFS 上的文件。 + +
+ + + +## 三、Hbase系统架构 + +### 3.1 系统架构 + +HBase 系统遵循 Master/Salve 架构,由三种不同类型的组件组成: + +**Zookeeper** + +1. 保证任何时候,集群中只有一个 Master; + +2. 存贮所有 Region 的寻址入口; + +3. 实时监控 Region Server 的状态,将 Region Server 的上线和下线信息实时通知给 Master; + +4. 存储 HBase 的 Schema,包括有哪些 Table,每个 Table 有哪些 Column Family 等信息。 + +**Master** + +1. 为 Region Server 分配 Region ; + +2. 负责 Region Server 的负载均衡 ; + +3. 发现失效的 Region Server 并重新分配其上的 Region; + +4. GFS 上的垃圾文件回收; + +5. 处理 Schema 的更新请求。 + +**Region Server** + +1. Region Server 负责维护 Master 分配给它的 Region ,并处理发送到 Region 上的 IO 请求; + +2. Region Server 负责切分在运行过程中变得过大的 Region。 + +
+ +### 3.2 组件间的协作 + + HBase 使用 ZooKeeper 作为分布式协调服务来维护集群中的服务器状态。 Zookeeper 负责维护可用服务列表,并提供服务故障通知等服务: + ++ 每个 Region Server 都会在 ZooKeeper 上创建一个临时节点,Master 通过 Zookeeper 的 Watcher 机制对节点进行监控,从而可以发现新加入的 Region Server 或故障退出的 Region Server; + ++ 所有 Masters 会竞争性地在 Zookeeper 上创建同一个临时节点,由于 Zookeeper 只能有一个同名节点,所以必然只有一个 Master 能够创建成功,此时该 Master 就是主 Master,主 Master 会定期向 Zookeeper 发送心跳。备用 Masters 则通过 Watcher 机制对主 HMaster 所在节点进行监听; + ++ 如果主 Master 未能定时发送心跳,则其持有的 Zookeeper 会话会过期,相应的临时节点也会被删除,这会触发定义在该节点上的 Watcher 事件,使得备用的 Master Servers 得到通知。所有备用的 Master Servers 在接到通知后,会再次去竞争性地创建临时节点,完成主 Master 的选举。 + +
+ + + +## 四、数据的读写流程简述 + +### 4.1 写入数据的流程 + +1. Client 向 Region Server 提交写请求; + +2. Region Server 找到目标 Region; + +3. Region 检查数据是否与 Schema 一致; + +4. 如果客户端没有指定版本,则获取当前系统时间作为数据版本; + +5. 将更新写入 WAL Log; + +6. 将更新写入 Memstore; + +7. 判断 Memstore 存储是否已满,如果存储已满则需要 flush 为 Store Hfile 文件。 + +> 更为详细写入流程可以参考:[HBase - 数据写入流程解析](http://hbasefly.com/2016/03/23/hbase_writer/) + + + +### 4.2 读取数据的流程 + +以下是客户端首次读写 HBase 上数据的流程: + +1. 客户端从 Zookeeper 获取 `META` 表所在的 Region Server; + +2. 客户端访问 `META` 表所在的 Region Server,从 `META` 表中查询到访问行键所在的 Region Server,之后客户端将缓存这些信息以及 `META` 表的位置; + +3. 客户端从行键所在的 Region Server 上获取数据。 + +如果再次读取,客户端将从缓存中获取行键所在的 Region Server。这样客户端就不需要再次查询 `META` 表,除非 Region 移动导致缓存失效,这样的话,则将会重新查询并更新缓存。 + +注:`META` 表是 HBase 中一张特殊的表,它保存了所有 Region 的位置信息,META 表自己的位置信息则存储在 ZooKeeper 上。 + +
+ +> 更为详细读取数据流程参考: +> +> [HBase 原理-数据读取流程解析](http://hbasefly.com/2016/12/21/hbase-getorscan/) +> +> [HBase 原理-迟到的‘数据读取流程部分细节](http://hbasefly.com/2017/06/11/hbase-scan-2/) + + + + + +## 参考资料 + +本篇文章内容主要参考自官方文档和以下两篇博客,图片也主要引用自以下两篇博客: + ++ [HBase Architectural Components](https://mapr.com/blog/in-depth-look-hbase-architecture/#.VdMxvWSqqko) + ++ [Hbase 系统架构及数据结构](https://www.open-open.com/lib/view/open1346821084631.html) + +官方文档: + ++ [Apache HBase ™ Reference Guide](https://hbase.apache.org/2.1/book.html) + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\350\277\207\346\273\244\345\231\250\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\350\277\207\346\273\244\345\231\250\350\257\246\350\247\243.md" new file mode 100644 index 0000000..85149be --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hbase\350\277\207\346\273\244\345\231\250\350\257\246\350\247\243.md" @@ -0,0 +1,445 @@ +# Hbase 过滤器详解 + + + + + +## 一、HBase过滤器简介 + +Hbase 提供了种类丰富的过滤器(filter)来提高数据处理的效率,用户可以通过内置或自定义的过滤器来对数据进行过滤,所有的过滤器都在服务端生效,即谓词下推(predicate push down)。这样可以保证过滤掉的数据不会被传送到客户端,从而减轻网络传输和客户端处理的压力。 + +
+ + + +## 二、过滤器基础 + +### 2.1 Filter接口和FilterBase抽象类 + +Filter 接口中定义了过滤器的基本方法,FilterBase 抽象类实现了 Filter 接口。所有内置的过滤器则直接或者间接继承自 FilterBase 抽象类。用户只需要将定义好的过滤器通过 `setFilter` 方法传递给 `Scan` 或 `put` 的实例即可。 + +```java +setFilter(Filter filter) +``` + +```java + // Scan 中定义的 setFilter + @Override + public Scan setFilter(Filter filter) { + super.setFilter(filter); + return this; + } +``` + +```java + // Get 中定义的 setFilter + @Override + public Get setFilter(Filter filter) { + super.setFilter(filter); + return this; + } +``` + +FilterBase 的所有子类过滤器如下:
+ +> 说明:上图基于当前时间点(2019.4)最新的 Hbase-2.1.4 ,下文所有说明均基于此版本。 + + + +### 2.2 过滤器分类 + +HBase 内置过滤器可以分为三类:分别是比较过滤器,专用过滤器和包装过滤器。分别在下面的三个小节中做详细的介绍。 + + + +## 三、比较过滤器 + +所有比较过滤器均继承自 `CompareFilter`。创建一个比较过滤器需要两个参数,分别是**比较运算符**和**比较器实例**。 + +```java + public CompareFilter(final CompareOp compareOp,final ByteArrayComparable comparator) { + this.compareOp = compareOp; + this.comparator = comparator; + } +``` + +### 3.1 比较运算符 + +- LESS (<) +- LESS_OR_EQUAL (<=) +- EQUAL (=) +- NOT_EQUAL (!=) +- GREATER_OR_EQUAL (>=) +- GREATER (>) +- NO_OP (排除所有符合条件的值) + +比较运算符均定义在枚举类 `CompareOperator` 中 + +```java +@InterfaceAudience.Public +public enum CompareOperator { + LESS, + LESS_OR_EQUAL, + EQUAL, + NOT_EQUAL, + GREATER_OR_EQUAL, + GREATER, + NO_OP, +} +``` + +> 注意:在 1.x 版本的 HBase 中,比较运算符定义在 `CompareFilter.CompareOp` 枚举类中,但在 2.0 之后这个类就被标识为 @deprecated ,并会在 3.0 移除。所以 2.0 之后版本的 HBase 需要使用 `CompareOperator` 这个枚举类。 +> + +### 3.2 比较器 + +所有比较器均继承自 `ByteArrayComparable` 抽象类,常用的有以下几种: + +
+ +- **BinaryComparator** : 使用 `Bytes.compareTo(byte [],byte [])` 按字典序比较指定的字节数组。 +- **BinaryPrefixComparator** : 按字典序与指定的字节数组进行比较,但只比较到这个字节数组的长度。 +- **RegexStringComparator** : 使用给定的正则表达式与指定的字节数组进行比较。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。 +- **SubStringComparator** : 测试给定的子字符串是否出现在指定的字节数组中,比较不区分大小写。仅支持 `EQUAL` 和 `NOT_EQUAL` 操作。 +- **NullComparator** :判断给定的值是否为空。 +- **BitComparator** :按位进行比较。 + +`BinaryPrefixComparator` 和 `BinaryComparator` 的区别不是很好理解,这里举例说明一下: + +在进行 `EQUAL` 的比较时,如果比较器传入的是 `abcd` 的字节数组,但是待比较数据是 `abcdefgh`: + ++ 如果使用的是 `BinaryPrefixComparator` 比较器,则比较以 `abcd` 字节数组的长度为准,即 `efgh` 不会参与比较,这时候认为 `abcd` 与 `abcdefgh` 是满足 `EQUAL` 条件的; ++ 如果使用的是 `BinaryComparator` 比较器,则认为其是不相等的。 + +### 3.3 比较过滤器种类 + +比较过滤器共有五个(Hbase 1.x 版本和 2.x 版本相同),见下图: + +
+ ++ **RowFilter** :基于行键来过滤数据; ++ **FamilyFilterr** :基于列族来过滤数据; ++ **QualifierFilterr** :基于列限定符(列名)来过滤数据; ++ **ValueFilterr** :基于单元格 (cell) 的值来过滤数据; ++ **DependentColumnFilter** :指定一个参考列来过滤其他列的过滤器,过滤的原则是基于参考列的时间戳来进行筛选 。 + +前四种过滤器的使用方法相同,均只要传递比较运算符和运算器实例即可构建,然后通过 `setFilter` 方法传递给 `scan`: + +```java + Filter filter = new RowFilter(CompareOperator.LESS_OR_EQUAL, + new BinaryComparator(Bytes.toBytes("xxx"))); + scan.setFilter(filter); +``` + +`DependentColumnFilter` 的使用稍微复杂一点,这里单独做下说明。 + +### 3.4 DependentColumnFilter + +可以把 `DependentColumnFilter` 理解为**一个 valueFilter 和一个时间戳过滤器的组合**。`DependentColumnFilter` 有三个带参构造器,这里选择一个参数最全的进行说明: + +```java +DependentColumnFilter(final byte [] family, final byte[] qualifier, + final boolean dropDependentColumn, final CompareOperator op, + final ByteArrayComparable valueComparator) +``` + ++ **family** :列族 ++ **qualifier** :列限定符(列名) ++ **dropDependentColumn** :决定参考列是否被包含在返回结果内,为 true 时表示参考列被返回,为 false 时表示被丢弃 ++ **op** :比较运算符 ++ **valueComparator** :比较器 + +这里举例进行说明: + +```java +DependentColumnFilter dependentColumnFilter = new DependentColumnFilter( + Bytes.toBytes("student"), + Bytes.toBytes("name"), + false, + CompareOperator.EQUAL, + new BinaryPrefixComparator(Bytes.toBytes("xiaolan"))); +``` + ++ 首先会去查找 `student:name` 中值以 `xiaolan` 开头的所有数据获得 ` 参考数据集 `,这一步等同于 valueFilter 过滤器; + ++ 其次再用参考数据集中所有数据的时间戳去检索其他列,获得时间戳相同的其他列的数据作为 ` 结果数据集 `,这一步等同于时间戳过滤器; + ++ 最后如果 `dropDependentColumn` 为 true,则返回 ` 参考数据集 `+` 结果数据集 `,若为 false,则抛弃参考数据集,只返回 ` 结果数据集 `。 + + + +## 四、专用过滤器 + +专用过滤器通常直接继承自 `FilterBase`,适用于范围更小的筛选规则。 + +### 4.1 单列列值过滤器 (SingleColumnValueFilter) + +基于某列(参考列)的值决定某行数据是否被过滤。其实例有以下方法: + ++ **setFilterIfMissing(boolean filterIfMissing)** :默认值为 false,即如果该行数据不包含参考列,其依然被包含在最后的结果中;设置为 true 时,则不包含; ++ **setLatestVersionOnly(boolean latestVersionOnly)** :默认为 true,即只检索参考列的最新版本数据;设置为 false,则检索所有版本数据。 + +```shell +SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter( + "student".getBytes(), + "name".getBytes(), + CompareOperator.EQUAL, + new SubstringComparator("xiaolan")); +singleColumnValueFilter.setFilterIfMissing(true); +scan.setFilter(singleColumnValueFilter); +``` + +### 4.2 单列列值排除器 (SingleColumnValueExcludeFilter) + +`SingleColumnValueExcludeFilter` 继承自上面的 `SingleColumnValueFilter`,过滤行为与其相反。 + +### 4.3 行键前缀过滤器 (PrefixFilter) + +基于 RowKey 值决定某行数据是否被过滤。 + +```java +PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes("xxx")); +scan.setFilter(prefixFilter); +``` + +### 4.4 列名前缀过滤器 (ColumnPrefixFilter) + +基于列限定符(列名)决定某行数据是否被过滤。 + +```java +ColumnPrefixFilter columnPrefixFilter = new ColumnPrefixFilter(Bytes.toBytes("xxx")); + scan.setFilter(columnPrefixFilter); +``` + +### 4.5 分页过滤器 (PageFilter) + +可以使用这个过滤器实现对结果按行进行分页,创建 PageFilter 实例的时候需要传入每页的行数。 + +```java +public PageFilter(final long pageSize) { + Preconditions.checkArgument(pageSize >= 0, "must be positive %s", pageSize); + this.pageSize = pageSize; + } +``` + +下面的代码体现了客户端实现分页查询的主要逻辑,这里对其进行一下解释说明: + +客户端进行分页查询,需要传递 `startRow`(起始 RowKey),知道起始 `startRow` 后,就可以返回对应的 pageSize 行数据。这里唯一的问题就是,对于第一次查询,显然 `startRow` 就是表格的第一行数据,但是之后第二次、第三次查询我们并不知道 `startRow`,只能知道上一次查询的最后一条数据的 RowKey(简单称之为 `lastRow`)。 + +我们不能将 `lastRow` 作为新一次查询的 `startRow` 传入,因为 scan 的查询区间是[startRow,endRow) ,即前开后闭区间,这样 `startRow` 在新的查询也会被返回,这条数据就重复了。 + +同时在不使用第三方数据库存储 RowKey 的情况下,我们是无法通过知道 `lastRow` 的下一个 RowKey 的,因为 RowKey 的设计可能是连续的也有可能是不连续的。 + +由于 Hbase 的 RowKey 是按照字典序进行排序的。这种情况下,就可以在 `lastRow` 后面加上 `0` ,作为 `startRow` 传入,因为按照字典序的规则,某个值加上 `0` 后的新值,在字典序上一定是这个值的下一个值,对于 HBase 来说下一个 RowKey 在字典序上一定也是等于或者大于这个新值的。 + +所以最后传入 `lastRow`+`0`,如果等于这个值的 RowKey 存在就从这个值开始 scan,否则从字典序的下一个 RowKey 开始 scan。 + +> 25 个字母以及数字字符,字典排序如下: +> +> `'0' < '1' < '2' < ... < '9' < 'a' < 'b' < ... < 'z'` + +分页查询主要实现逻辑: + +```java +byte[] POSTFIX = new byte[] { 0x00 }; +Filter filter = new PageFilter(15); + +int totalRows = 0; +byte[] lastRow = null; +while (true) { + Scan scan = new Scan(); + scan.setFilter(filter); + if (lastRow != null) { + // 如果不是首行 则 lastRow + 0 + byte[] startRow = Bytes.add(lastRow, POSTFIX); + System.out.println("start row: " + + Bytes.toStringBinary(startRow)); + scan.withStartRow(startRow); + } + ResultScanner scanner = table.getScanner(scan); + int localRows = 0; + Result result; + while ((result = scanner.next()) != null) { + System.out.println(localRows++ + ": " + result); + totalRows++; + lastRow = result.getRow(); + } + scanner.close(); + //最后一页,查询结束 + if (localRows == 0) break; +} +System.out.println("total rows: " + totalRows); +``` + +>需要注意的是在多台 Regin Services 上执行分页过滤的时候,由于并行执行的过滤器不能共享它们的状态和边界,所以有可能每个过滤器都会在完成扫描前获取了 PageCount 行的结果,这种情况下会返回比分页条数更多的数据,分页过滤器就有失效的可能。 + + + +### 4.6 时间戳过滤器 (TimestampsFilter) + +```java +List list = new ArrayList<>(); +list.add(1554975573000L); +TimestampsFilter timestampsFilter = new TimestampsFilter(list); +scan.setFilter(timestampsFilter); +``` + +### 4.7 首次行键过滤器 (FirstKeyOnlyFilter) + +`FirstKeyOnlyFilter` 只扫描每行的第一列,扫描完第一列后就结束对当前行的扫描,并跳转到下一行。相比于全表扫描,其性能更好,通常用于行数统计的场景,因为如果某一行存在,则行中必然至少有一列。 + +```java +FirstKeyOnlyFilter firstKeyOnlyFilter = new FirstKeyOnlyFilter(); +scan.set(firstKeyOnlyFilter); +``` + +## 五、包装过滤器 + +包装过滤器就是通过包装其他过滤器以实现某些拓展的功能。 + +### 5.1 SkipFilter过滤器 + +`SkipFilter` 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,则拓展过滤整行数据。下面是一个使用示例: + +```java +// 定义 ValueFilter 过滤器 +Filter filter1 = new ValueFilter(CompareOperator.NOT_EQUAL, + new BinaryComparator(Bytes.toBytes("xxx"))); +// 使用 SkipFilter 进行包装 +Filter filter2 = new SkipFilter(filter1); +``` + + + +### 5.2 WhileMatchFilter过滤器 + +`WhileMatchFilter` 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,`WhileMatchFilter` 则结束本次扫描,返回已经扫描到的结果。下面是其使用示例: + +```java +Filter filter1 = new RowFilter(CompareOperator.NOT_EQUAL, + new BinaryComparator(Bytes.toBytes("rowKey4"))); + +Scan scan = new Scan(); +scan.setFilter(filter1); +ResultScanner scanner1 = table.getScanner(scan); +for (Result result : scanner1) { + for (Cell cell : result.listCells()) { + System.out.println(cell); + } +} +scanner1.close(); + +System.out.println("--------------------"); + +// 使用 WhileMatchFilter 进行包装 +Filter filter2 = new WhileMatchFilter(filter1); + +scan.setFilter(filter2); +ResultScanner scanner2 = table.getScanner(scan); +for (Result result : scanner1) { + for (Cell cell : result.listCells()) { + System.out.println(cell); + } +} +scanner2.close(); +``` + +```properties +rowKey0/student:name/1555035006994/Put/vlen=8/seqid=0 +rowKey1/student:name/1555035007019/Put/vlen=8/seqid=0 +rowKey2/student:name/1555035007025/Put/vlen=8/seqid=0 +rowKey3/student:name/1555035007037/Put/vlen=8/seqid=0 +rowKey5/student:name/1555035007051/Put/vlen=8/seqid=0 +rowKey6/student:name/1555035007057/Put/vlen=8/seqid=0 +rowKey7/student:name/1555035007062/Put/vlen=8/seqid=0 +rowKey8/student:name/1555035007068/Put/vlen=8/seqid=0 +rowKey9/student:name/1555035007073/Put/vlen=8/seqid=0 +-------------------- +rowKey0/student:name/1555035006994/Put/vlen=8/seqid=0 +rowKey1/student:name/1555035007019/Put/vlen=8/seqid=0 +rowKey2/student:name/1555035007025/Put/vlen=8/seqid=0 +rowKey3/student:name/1555035007037/Put/vlen=8/seqid=0 +``` + +可以看到被包装后,只返回了 `rowKey4` 之前的数据。 + +## 六、FilterList + +以上都是讲解单个过滤器的作用,当需要多个过滤器共同作用于一次查询的时候,就需要使用 `FilterList`。`FilterList` 支持通过构造器或者 `addFilter` 方法传入多个过滤器。 + +```java +// 构造器传入 +public FilterList(final Operator operator, final List filters) +public FilterList(final List filters) +public FilterList(final Filter... filters) + +// 方法传入 + public void addFilter(List filters) + public void addFilter(Filter filter) +``` + +多个过滤器组合的结果由 `operator` 参数定义 ,其可选参数定义在 `Operator` 枚举类中。只有 `MUST_PASS_ALL` 和 `MUST_PASS_ONE` 两个可选的值: + ++ **MUST_PASS_ALL** :相当于 AND,必须所有的过滤器都通过才认为通过; ++ **MUST_PASS_ONE** :相当于 OR,只有要一个过滤器通过则认为通过。 + +```java +@InterfaceAudience.Public + public enum Operator { + /** !AND */ + MUST_PASS_ALL, + /** !OR */ + MUST_PASS_ONE + } +``` + +使用示例如下: + +```java +List filters = new ArrayList(); + +Filter filter1 = new RowFilter(CompareOperator.GREATER_OR_EQUAL, + new BinaryComparator(Bytes.toBytes("XXX"))); +filters.add(filter1); + +Filter filter2 = new RowFilter(CompareOperator.LESS_OR_EQUAL, + new BinaryComparator(Bytes.toBytes("YYY"))); +filters.add(filter2); + +Filter filter3 = new QualifierFilter(CompareOperator.EQUAL, + new RegexStringComparator("ZZZ")); +filters.add(filter3); + +FilterList filterList = new FilterList(filters); + +Scan scan = new Scan(); +scan.setFilter(filterList); +``` + + + +## 参考资料 + +[HBase: The Definitive Guide _> Chapter 4. Client API: Advanced Features](https://www.oreilly.com/library/view/hbase-the-definitive/9781449314682/ch04.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HiveCLI\345\222\214Beeline\345\221\275\344\273\244\350\241\214\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HiveCLI\345\222\214Beeline\345\221\275\344\273\244\350\241\214\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" new file mode 100644 index 0000000..2dd706b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/HiveCLI\345\222\214Beeline\345\221\275\344\273\244\350\241\214\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" @@ -0,0 +1,279 @@ +# Hive CLI和Beeline命令行的基本使用 + + + +## 一、Hive CLI + +### 1.1 Help + +使用 `hive -H` 或者 `hive --help` 命令可以查看所有命令的帮助,显示如下: + +``` +usage: hive + -d,--define Variable subsitution to apply to hive + commands. e.g. -d A=B or --define A=B --定义用户自定义变量 + --database Specify the database to use -- 指定使用的数据库 + -e SQL from command line -- 执行指定的 SQL + -f SQL from files --执行 SQL 脚本 + -H,--help Print help information -- 打印帮助信息 + --hiveconf Use value for given property --自定义配置 + --hivevar Variable subsitution to apply to hive --自定义变量 + commands. e.g. --hivevar A=B + -i Initialization SQL file --在进入交互模式之前运行初始化脚本 + -S,--silent Silent mode in interactive shell --静默模式 + -v,--verbose Verbose mode (echo executed SQL to the console) --详细模式 +``` + +### 1.2 交互式命令行 + +直接使用 `Hive` 命令,不加任何参数,即可进入交互式命令行。 + +### 1.3 执行SQL命令 + +在不进入交互式命令行的情况下,可以使用 `hive -e ` 执行 SQL 命令。 + +```sql +hive -e 'select * from emp'; +``` + +
+ + + +### 1.4 执行SQL脚本 + +用于执行的 sql 脚本可以在本地文件系统,也可以在 HDFS 上。 + +```shell +# 本地文件系统 +hive -f /usr/file/simple.sql; + +# HDFS文件系统 +hive -f hdfs://hadoop001:8020/tmp/simple.sql; +``` + +其中 `simple.sql` 内容如下: + +```sql +select * from emp; +``` + +### 1.5 配置Hive变量 + +可以使用 `--hiveconf` 设置 Hive 运行时的变量。 + +```sql +hive -e 'select * from emp' \ +--hiveconf hive.exec.scratchdir=/tmp/hive_scratch \ +--hiveconf mapred.reduce.tasks=4; +``` + +> hive.exec.scratchdir:指定 HDFS 上目录位置,用于存储不同 map/reduce 阶段的执行计划和这些阶段的中间输出结果。 + +### 1.6 配置文件启动 + +使用 `-i` 可以在进入交互模式之前运行初始化脚本,相当于指定配置文件启动。 + +```shell +hive -i /usr/file/hive-init.conf; +``` + +其中 `hive-init.conf` 的内容如下: + +```sql +set hive.exec.mode.local.auto = true; +``` + +> hive.exec.mode.local.auto 默认值为 false,这里设置为 true ,代表开启本地模式。 + +### 1.7 用户自定义变量 + +`--define ` 和 `--hivevar ` 在功能上是等价的,都是用来实现自定义变量,这里给出一个示例: + +定义变量: + +```sql +hive --define n=ename --hiveconf --hivevar j=job; +``` + +在查询中引用自定义变量: + +```sql +# 以下两条语句等价 +hive > select ${n} from emp; +hive > select ${hivevar:n} from emp; + +# 以下两条语句等价 +hive > select ${j} from emp; +hive > select ${hivevar:j} from emp; +``` + +结果如下: + +
+ +## 二、Beeline + +### 2.1 HiveServer2 + +Hive 内置了 HiveServer 和 HiveServer2 服务,两者都允许客户端使用多种编程语言进行连接,但是 HiveServer 不能处理多个客户端的并发请求,所以产生了 HiveServer2。 + +HiveServer2(HS2)允许远程客户端可以使用各种编程语言向 Hive 提交请求并检索结果,支持多客户端并发访问和身份验证。HS2 是由多个服务组成的单个进程,其包括基于 Thrift 的 Hive 服务(TCP 或 HTTP)和用于 Web UI 的 Jetty Web 服务器。 + + HiveServer2 拥有自己的 CLI(Beeline),Beeline 是一个基于 SQLLine 的 JDBC 客户端。由于 HiveServer2 是 Hive 开发维护的重点 (Hive0.15 后就不再支持 hiveserver),所以 Hive CLI 已经不推荐使用了,官方更加推荐使用 Beeline。 + +### 2.1 Beeline + +Beeline 拥有更多可使用参数,可以使用 `beeline --help` 查看,完整参数如下: + +```properties +Usage: java org.apache.hive.cli.beeline.BeeLine + -u the JDBC URL to connect to + -r reconnect to last saved connect url (in conjunction with !save) + -n the username to connect as + -p the password to connect as + -d the driver class to use + -i script file for initialization + -e query that should be executed + -f script file that should be executed + -w (or) --password-file the password file to read password from + --hiveconf property=value Use value for given property + --hivevar name=value hive variable name and value + This is Hive specific settings in which variables + can be set at session level and referenced in Hive + commands or queries. + --property-file= the file to read connection properties (url, driver, user, password) from + --color=[true/false] control whether color is used for display + --showHeader=[true/false] show column names in query results + --headerInterval=ROWS; the interval between which heades are displayed + --fastConnect=[true/false] skip building table/column list for tab-completion + --autoCommit=[true/false] enable/disable automatic transaction commit + --verbose=[true/false] show verbose error messages and debug info + --showWarnings=[true/false] display connection warnings + --showNestedErrs=[true/false] display nested errors + --numberFormat=[pattern] format numbers using DecimalFormat pattern + --force=[true/false] continue running script even after errors + --maxWidth=MAXWIDTH the maximum width of the terminal + --maxColumnWidth=MAXCOLWIDTH the maximum width to use when displaying columns + --silent=[true/false] be more silent + --autosave=[true/false] automatically save preferences + --outputformat=[table/vertical/csv2/tsv2/dsv/csv/tsv] format mode for result display + --incrementalBufferRows=NUMROWS the number of rows to buffer when printing rows on stdout, + defaults to 1000; only applicable if --incremental=true + and --outputformat=table + --truncateTable=[true/false] truncate table column when it exceeds length + --delimiterForDSV=DELIMITER specify the delimiter for delimiter-separated values output format (default: |) + --isolation=LEVEL set the transaction isolation level + --nullemptystring=[true/false] set to true to get historic behavior of printing null as empty string + --maxHistoryRows=MAXHISTORYROWS The maximum number of rows to store beeline history. + --convertBinaryArrayToString=[true/false] display binary column data as string or as byte array + --help display this message + +``` + +### 2.3 常用参数 + +在 Hive CLI 中支持的参数,Beeline 都支持,常用的参数如下。更多参数说明可以参见官方文档 [Beeline Command Options](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients#HiveServer2Clients-Beeline%E2%80%93NewCommandLineShell) + +| 参数 | 说明 | +| -------------------------------------- | ------------------------------------------------------------ | +| **-u \** | 数据库地址 | +| **-n \** | 用户名 | +| **-p \** | 密码 | +| **-d \** | 驱动 (可选) | +| **-e \** | 执行 SQL 命令 | +| **-f \** | 执行 SQL 脚本 | +| **-i (or)--init \** | 在进入交互模式之前运行初始化脚本 | +| **--property-file \** | 指定配置文件 | +| **--hiveconf** *property**=**value* | 指定配置属性 | +| **--hivevar** *name**=**value* | 用户自定义属性,在会话级别有效 | + +示例: 使用用户名和密码连接 Hive + +```shell +$ beeline -u jdbc:hive2://localhost:10000 -n username -p password +``` + +​ + +## 三、Hive配置 + +可以通过三种方式对 Hive 的相关属性进行配置,分别介绍如下: + +### 3.1 配置文件 + +方式一为使用配置文件,使用配置文件指定的配置是永久有效的。Hive 有以下三个可选的配置文件: + ++ hive-site.xml :Hive 的主要配置文件; + ++ hivemetastore-site.xml: 关于元数据的配置; ++ hiveserver2-site.xml:关于 HiveServer2 的配置。 + +示例如下,在 hive-site.xml 配置 `hive.exec.scratchdir`: + +```xml + + hive.exec.scratchdir + /tmp/mydir + Scratch space for Hive jobs + +``` + +### 3.2 hiveconf + +方式二为在启动命令行 (Hive CLI / Beeline) 的时候使用 `--hiveconf` 指定配置,这种方式指定的配置作用于整个 Session。 + +``` +hive --hiveconf hive.exec.scratchdir=/tmp/mydir +``` + +### 3.3 set + +方式三为在交互式环境下 (Hive CLI / Beeline),使用 set 命令指定。这种设置的作用范围也是 Session 级别的,配置对于执行该命令后的所有命令生效。set 兼具设置参数和查看参数的功能。如下: + +```shell +0: jdbc:hive2://hadoop001:10000> set hive.exec.scratchdir=/tmp/mydir; +No rows affected (0.025 seconds) +0: jdbc:hive2://hadoop001:10000> set hive.exec.scratchdir; ++----------------------------------+--+ +| set | ++----------------------------------+--+ +| hive.exec.scratchdir=/tmp/mydir | ++----------------------------------+--+ +``` + +### 3.4 配置优先级 + +配置的优先顺序如下 (由低到高): +`hive-site.xml` - >` hivemetastore-site.xml `- > `hiveserver2-site.xml` - >` -- hiveconf`- > `set` + +### 3.5 配置参数 + +Hive 可选的配置参数非常多,在用到时查阅官方文档即可[AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration) + + + +## 参考资料 + +1. [HiveServer2 Clients](https://cwiki.apache.org/confluence/display/Hive/HiveServer2+Clients) +2. [LanguageManual Cli](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Cli) +3. [AdminManual Configuration](https://cwiki.apache.org/confluence/display/Hive/AdminManual+Configuration) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\210\206\345\214\272\350\241\250\345\222\214\345\210\206\346\241\266\350\241\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\210\206\345\214\272\350\241\250\345\222\214\345\210\206\346\241\266\350\241\250.md" new file mode 100644 index 0000000..633d07b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\210\206\345\214\272\350\241\250\345\222\214\345\210\206\346\241\266\350\241\250.md" @@ -0,0 +1,168 @@ +# Hive分区表和分桶表 + + + + +## 一、分区表 + +### 1.1 概念 + +Hive 中的表对应为 HDFS 上的指定目录,在查询数据时候,默认会对全表进行扫描,这样时间和性能的消耗都非常大。 + +**分区为 HDFS 上表目录的子目录**,数据按照分区存储在子目录中。如果查询的 `where` 字句的中包含分区条件,则直接从该分区去查找,而不是扫描整个表目录,合理的分区设计可以极大提高查询速度和性能。 + +>这里说明一下分区表并 Hive 独有的概念,实际上这个概念非常常见。比如在我们常用的 Oracle 数据库中,当表中的数据量不断增大,查询数据的速度就会下降,这时也可以对表进行分区。表进行分区后,逻辑上表仍然是一张完整的表,只是将表中的数据存放到多个表空间(物理文件上),这样查询数据时,就不必要每次都扫描整张表,从而提升查询性能。 + +### 1.2 使用场景 + +通常,在管理大规模数据集的时候都需要进行分区,比如将日志文件按天进行分区,从而保证数据细粒度的划分,使得查询性能得到提升。 + +### 1.3 创建分区表 + +在 Hive 中可以使用 `PARTITIONED BY` 子句创建分区表。表可以包含一个或多个分区列,程序会为分区列中的每个不同值组合创建单独的数据目录。下面的我们创建一张雇员表作为测试: + +```shell + CREATE EXTERNAL TABLE emp_partition( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + PARTITIONED BY (deptno INT) -- 按照部门编号进行分区 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_partition'; +``` + +### 1.4 加载数据到分区表 + +加载数据到分区表时候必须要指定数据所处的分区: + +```shell +# 加载部门编号为20的数据到表中 +LOAD DATA LOCAL INPATH "/usr/file/emp20.txt" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=20) +# 加载部门编号为30的数据到表中 +LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=30) +``` + +### 1.5 查看分区目录 + +这时候我们直接查看表目录,可以看到表目录下存在两个子目录,分别是 `deptno=20` 和 `deptno=30`,这就是分区目录,分区目录下才是我们加载的数据文件。 + +```shell +# hadoop fs -ls hdfs://hadoop001:8020/hive/emp_partition/ +``` + +这时候当你的查询语句的 `where` 包含 `deptno=20`,则就去对应的分区目录下进行查找,而不用扫描全表。 + +
+ + + +## 二、分桶表 + +### 1.1 简介 + +分区提供了一个隔离数据和优化查询的可行方案,但是并非所有的数据集都可以形成合理的分区,分区的数量也不是越多越好,过多的分区条件可能会导致很多分区上没有数据。同时 Hive 会限制动态分区可以创建的最大分区数,用来避免过多分区文件对文件系统产生负担。鉴于以上原因,Hive 还提供了一种更加细粒度的数据拆分方案:分桶表 (bucket Table)。 + +分桶表会将指定列的值进行哈希散列,并对 bucket(桶数量)取余,然后存储到对应的 bucket(桶)中。 + +### 1.2 理解分桶表 + +单从概念上理解分桶表可能会比较晦涩,其实和分区一样,分桶这个概念同样不是 Hive 独有的,对于 Java 开发人员而言,这可能是一个每天都会用到的概念,因为 Hive 中的分桶概念和 Java 数据结构中的 HashMap 的分桶概念是一致的。 + +当调用 HashMap 的 put() 方法存储数据时,程序会先对 key 值调用 hashCode() 方法计算出 hashcode,然后对数组长度取模计算出 index,最后将数据存储在数组 index 位置的链表上,链表达到一定阈值后会转换为红黑树 (JDK1.8+)。下图为 HashMap 的数据结构图: + +
+ +> 图片引用自:[HashMap vs. Hashtable](http://www.itcuties.com/java/hashmap-hashtable/) + +### 1.3 创建分桶表 + +在 Hive 中,我们可以通过 `CLUSTERED BY` 指定分桶列,并通过 `SORTED BY` 指定桶中数据的排序参考列。下面为分桶表建表语句示例: + +```sql + CREATE EXTERNAL TABLE emp_bucket( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2), + deptno INT) + CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个 bucket 中 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_bucket'; +``` + +### 1.4 加载数据到分桶表 + +这里直接使用 `Load` 语句向分桶表加载数据,数据时可以加载成功的,但是数据并不会分桶。 + +这是由于分桶的实质是对指定字段做了 hash 散列然后存放到对应文件中,这意味着向分桶表中插入数据是必然要通过 MapReduce,且 Reducer 的数量必须等于分桶的数量。由于以上原因,分桶表的数据通常只能使用 CTAS(CREATE TABLE AS SELECT) 方式插入,因为 CTAS 操作会触发 MapReduce。加载数据步骤如下: + +#### 1. 设置强制分桶 + +```sql +set hive.enforce.bucketing = true; --Hive 2.x 不需要这一步 +``` +在 Hive 0.x and 1.x 版本,必须使用设置 `hive.enforce.bucketing = true`,表示强制分桶,允许程序根据表结构自动选择正确数量的 Reducer 和 cluster by column 来进行分桶。 + +#### 2. CTAS导入数据 + +```sql +INSERT INTO TABLE emp_bucket SELECT * FROM emp; --这里的 emp 表就是一张普通的雇员表 +``` + +可以从执行日志看到 CTAS 触发 MapReduce 操作,且 Reducer 数量和建表时候指定 bucket 数量一致: + +
+ +### 1.5 查看分桶文件 + +bucket(桶) 本质上就是表目录下的具体文件: + +
+ + + +## 三、分区表和分桶表结合使用 + +分区表和分桶表的本质都是将数据按照不同粒度进行拆分,从而使得在查询时候不必扫描全表,只需要扫描对应的分区或分桶,从而提升查询效率。两者可以结合起来使用,从而保证表数据在不同粒度上都能得到合理的拆分。下面是 Hive 官方给出的示例: + +```sql +CREATE TABLE page_view_bucketed( + viewTime INT, + userid BIGINT, + page_url STRING, + referrer_url STRING, + ip STRING ) + PARTITIONED BY(dt STRING) + CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS + ROW FORMAT DELIMITED + FIELDS TERMINATED BY '\001' + COLLECTION ITEMS TERMINATED BY '\002' + MAP KEYS TERMINATED BY '\003' + STORED AS SEQUENCEFILE; +``` + +此时导入数据时需要指定分区: + +```shell +INSERT OVERWRITE page_view_bucketed +PARTITION (dt='2009-02-25') +SELECT * FROM page_view WHERE dt='2009-02-25'; +``` + + + +## 参考资料 + +1. [LanguageManual DDL BucketedTables](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL+BucketedTables) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DDL\346\223\215\344\275\234.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DDL\346\223\215\344\275\234.md" new file mode 100644 index 0000000..cc72196 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DDL\346\223\215\344\275\234.md" @@ -0,0 +1,450 @@ +# Hive常用DDL操作 + + + +## 一、Database + +### 1.1 查看数据列表 + +```sql +show databases; +``` + +
+ +### 1.2 使用数据库 + +```sql +USE database_name; +``` + +### 1.3 新建数据库 + +语法: + +```sql +CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name --DATABASE|SCHEMA 是等价的 + [COMMENT database_comment] --数据库注释 + [LOCATION hdfs_path] --存储在 HDFS 上的位置 + [WITH DBPROPERTIES (property_name=property_value, ...)]; --指定额外属性 +``` + +示例: + +```sql +CREATE DATABASE IF NOT EXISTS hive_test + COMMENT 'hive database for test' + WITH DBPROPERTIES ('create'='heibaiying'); +``` + + + +### 1.4 查看数据库信息 + +语法: + +```sql +DESC DATABASE [EXTENDED] db_name; --EXTENDED 表示是否显示额外属性 +``` + +示例: + +```sql +DESC DATABASE EXTENDED hive_test; +``` + + + +### 1.5 删除数据库 + +语法: + +```sql +DROP (DATABASE|SCHEMA) [IF EXISTS] database_name [RESTRICT|CASCADE]; +``` + ++ 默认行为是 RESTRICT,如果数据库中存在表则删除失败。要想删除库及其中的表,可以使用 CASCADE 级联删除。 + +示例: + +```sql + DROP DATABASE IF EXISTS hive_test CASCADE; +``` + + + +## 二、创建表 + +### 2.1 建表语法 + +```sql +CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name --表名 + [(col_name data_type [COMMENT col_comment], + ... [constraint_specification])] --列名 列数据类型 + [COMMENT table_comment] --表描述 + [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)] --分区表分区规则 + [ + CLUSTERED BY (col_name, col_name, ...) + [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS + ] --分桶表分桶规则 + [SKEWED BY (col_name, col_name, ...) ON ((col_value, col_value, ...), (col_value, col_value, ...), ...) + [STORED AS DIRECTORIES] + ] --指定倾斜列和值 + [ + [ROW FORMAT row_format] + [STORED AS file_format] + | STORED BY 'storage.handler.class.name' [WITH SERDEPROPERTIES (...)] + ] -- 指定行分隔符、存储文件格式或采用自定义存储格式 + [LOCATION hdfs_path] -- 指定表的存储位置 + [TBLPROPERTIES (property_name=property_value, ...)] --指定表的属性 + [AS select_statement]; --从查询结果创建表 +``` + +### 2.2 内部表 + +```sql + CREATE TABLE emp( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2), + deptno INT) + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; +``` + +### 2.3 外部表 + +```sql + CREATE EXTERNAL TABLE emp_external( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2), + deptno INT) + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_external'; +``` + +使用 `desc format emp_external` 命令可以查看表的详细信息如下: + +
+ +### 2.4 分区表 + +```sql + CREATE EXTERNAL TABLE emp_partition( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + PARTITIONED BY (deptno INT) -- 按照部门编号进行分区 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_partition'; +``` + +### 2.5 分桶表 + +```sql + CREATE EXTERNAL TABLE emp_bucket( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2), + deptno INT) + CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个 bucket 中 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_bucket'; +``` + +### 2.6 倾斜表 + +通过指定一个或者多个列经常出现的值(严重偏斜),Hive 会自动将涉及到这些值的数据拆分为单独的文件。在查询时,如果涉及到倾斜值,它就直接从独立文件中获取数据,而不是扫描所有文件,这使得性能得到提升。 + +```sql + CREATE EXTERNAL TABLE emp_skewed( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + SKEWED BY (empno) ON (66,88,100) --指定 empno 的倾斜值 66,88,100 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t" + LOCATION '/hive/emp_skewed'; +``` + +### 2.7 临时表 + +临时表仅对当前 session 可见,临时表的数据将存储在用户的暂存目录中,并在会话结束后删除。如果临时表与永久表表名相同,则对该表名的任何引用都将解析为临时表,而不是永久表。临时表还具有以下两个限制: + ++ 不支持分区列; ++ 不支持创建索引。 + +```sql + CREATE TEMPORARY TABLE emp_temp( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; +``` + +### 2.8 CTAS创建表 + +支持从查询语句的结果创建表: + +```sql +CREATE TABLE emp_copy AS SELECT * FROM emp WHERE deptno='20'; +``` + +### 2.9 复制表结构 + +语法: + +```sql +CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name --创建表表名 + LIKE existing_table_or_view_name --被复制表的表名 + [LOCATION hdfs_path]; --存储位置 +``` + +示例: + +```sql +CREATE TEMPORARY EXTERNAL TABLE IF NOT EXISTS emp_co LIKE emp +``` + + + +### 2.10 加载数据到表 + +加载数据到表中属于 DML 操作,这里为了方便大家测试,先简单介绍一下加载本地数据到表中: + +```sql +-- 加载数据到 emp 表中 +load data local inpath "/usr/file/emp.txt" into table emp; +``` + +其中 emp.txt 的内容如下,你可以直接复制使用,也可以到本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载: + +```txt +7369 SMITH CLERK 7902 1980-12-17 00:00:00 800.00 20 +7499 ALLEN SALESMAN 7698 1981-02-20 00:00:00 1600.00 300.00 30 +7521 WARD SALESMAN 7698 1981-02-22 00:00:00 1250.00 500.00 30 +7566 JONES MANAGER 7839 1981-04-02 00:00:00 2975.00 20 +7654 MARTIN SALESMAN 7698 1981-09-28 00:00:00 1250.00 1400.00 30 +7698 BLAKE MANAGER 7839 1981-05-01 00:00:00 2850.00 30 +7782 CLARK MANAGER 7839 1981-06-09 00:00:00 2450.00 10 +7788 SCOTT ANALYST 7566 1987-04-19 00:00:00 1500.00 20 +7839 KING PRESIDENT 1981-11-17 00:00:00 5000.00 10 +7844 TURNER SALESMAN 7698 1981-09-08 00:00:00 1500.00 0.00 30 +7876 ADAMS CLERK 7788 1987-05-23 00:00:00 1100.00 20 +7900 JAMES CLERK 7698 1981-12-03 00:00:00 950.00 30 +7902 FORD ANALYST 7566 1981-12-03 00:00:00 3000.00 20 +7934 MILLER CLERK 7782 1982-01-23 00:00:00 1300.00 10 +``` + +加载后可查询表中数据: + +
+ + + +## 三、修改表 + +### 3.1 重命名表 + +语法: + +```sql +ALTER TABLE table_name RENAME TO new_table_name; +``` + +示例: + +```sql +ALTER TABLE emp_temp RENAME TO new_emp; --把 emp_temp 表重命名为 new_emp +``` + + + +### 3.2 修改列 + +语法: + +```sql +ALTER TABLE table_name [PARTITION partition_spec] CHANGE [COLUMN] col_old_name col_new_name column_type + [COMMENT col_comment] [FIRST|AFTER column_name] [CASCADE|RESTRICT]; +``` + +示例: + +```sql +-- 修改字段名和类型 +ALTER TABLE emp_temp CHANGE empno empno_new INT; + +-- 修改字段 sal 的名称 并将其放置到 empno 字段后 +ALTER TABLE emp_temp CHANGE sal sal_new decimal(7,2) AFTER ename; + +-- 为字段增加注释 +ALTER TABLE emp_temp CHANGE mgr mgr_new INT COMMENT 'this is column mgr'; +``` + + + +### 3.3 新增列 + +示例: + +```sql +ALTER TABLE emp_temp ADD COLUMNS (address STRING COMMENT 'home address'); +``` + + + +## 四、清空表/删除表 + +### 4.1 清空表 + +语法: + +```sql +-- 清空整个表或表指定分区中的数据 +TRUNCATE TABLE table_name [PARTITION (partition_column = partition_col_value, ...)]; +``` + ++ 目前只有内部表才能执行 TRUNCATE 操作,外部表执行时会抛出异常 `Cannot truncate non-managed table XXXX`。 + +示例: + +```sql +TRUNCATE TABLE emp_mgt_ptn PARTITION (deptno=20); +``` + + + +### 4.2 删除表 + +语法: + +```sql +DROP TABLE [IF EXISTS] table_name [PURGE]; +``` + ++ 内部表:不仅会删除表的元数据,同时会删除 HDFS 上的数据; ++ 外部表:只会删除表的元数据,不会删除 HDFS 上的数据; ++ 删除视图引用的表时,不会给出警告(但视图已经无效了,必须由用户删除或重新创建)。 + + + +## 五、其他命令 + +### 5.1 Describe + +查看数据库: + +```sql +DESCRIBE|Desc DATABASE [EXTENDED] db_name; --EXTENDED 是否显示额外属性 +``` + +查看表: + +```sql +DESCRIBE|Desc [EXTENDED|FORMATTED] table_name --FORMATTED 以友好的展现方式查看表详情 +``` + + + +### 5.2 Show + +**1. 查看数据库列表** + +```sql +-- 语法 +SHOW (DATABASES|SCHEMAS) [LIKE 'identifier_with_wildcards']; + +-- 示例: +SHOW DATABASES like 'hive*'; +``` + +LIKE 子句允许使用正则表达式进行过滤,但是 SHOW 语句当中的 LIKE 子句只支持 `*`(通配符)和 `|`(条件或)两个符号。例如 `employees`,`emp *`,`emp * | * ees`,所有这些都将匹配名为 `employees` 的数据库。 + +**2. 查看表的列表** + +```sql +-- 语法 +SHOW TABLES [IN database_name] ['identifier_with_wildcards']; + +-- 示例 +SHOW TABLES IN default; +``` + +**3. 查看视图列表** + +```sql +SHOW VIEWS [IN/FROM database_name] [LIKE 'pattern_with_wildcards']; --仅支持 Hive 2.2.0 + +``` + +**4. 查看表的分区列表** + +```sql +SHOW PARTITIONS table_name; +``` + +**5. 查看表/视图的创建语句** + +```sql +SHOW CREATE TABLE ([db_name.]table_name|view_name); +``` + + + +## 参考资料 + +[LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DML\346\223\215\344\275\234.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DML\346\223\215\344\275\234.md" new file mode 100644 index 0000000..2d67973 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\345\270\270\347\224\250DML\346\223\215\344\275\234.md" @@ -0,0 +1,329 @@ +# Hive 常用DML操作 + + + + +## 一、加载文件数据到表 + +### 1.1 语法 + +```shell +LOAD DATA [LOCAL] INPATH 'filepath' [OVERWRITE] +INTO TABLE tablename [PARTITION (partcol1=val1, partcol2=val2 ...)] +``` + +- `LOCAL` 关键字代表从本地文件系统加载文件,省略则代表从 HDFS 上加载文件: ++ 从本地文件系统加载文件时, `filepath` 可以是绝对路径也可以是相对路径 (建议使用绝对路径); + ++ 从 HDFS 加载文件时候,`filepath` 为文件完整的 URL 地址:如 `hdfs://namenode:port/user/hive/project/ data1` + +- `filepath` 可以是文件路径 (在这种情况下 Hive 会将文件移动到表中),也可以目录路径 (在这种情况下,Hive 会将该目录中的所有文件移动到表中); + +- 如果使用 OVERWRITE 关键字,则将删除目标表(或分区)的内容,使用新的数据填充;不使用此关键字,则数据以追加的方式加入; + +- 加载的目标可以是表或分区。如果是分区表,则必须指定加载数据的分区; + +- 加载文件的格式必须与建表时使用 ` STORED AS` 指定的存储格式相同。 + +> 使用建议: +> +> **不论是本地路径还是 URL 都建议使用完整的**。虽然可以使用不完整的 URL 地址,此时 Hive 将使用 hadoop 中的 fs.default.name 配置来推断地址,但是为避免不必要的错误,建议使用完整的本地路径或 URL 地址; +> +> **加载对象是分区表时建议显示指定分区**。在 Hive 3.0 之后,内部将加载 (LOAD) 重写为 INSERT AS SELECT,此时如果不指定分区,INSERT AS SELECT 将假设最后一组列是分区列,如果该列不是表定义的分区,它将抛出错误。为避免错误,还是建议显示指定分区。 + +### 1.2 示例 + +新建分区表: + +```sql + CREATE TABLE emp_ptn( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + PARTITIONED BY (deptno INT) -- 按照部门编号进行分区 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; +``` + +从 HDFS 上加载数据到分区表: + +```sql +LOAD DATA INPATH "hdfs://hadoop001:8020/mydir/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=20); +``` + +> emp.txt 文件可在本仓库的 resources 目录中下载 + +加载后表中数据如下,分区列 deptno 全部赋值成 20: + +
+ +## 二、查询结果插入到表 + +### 2.1 语法 + +```sql +INSERT OVERWRITE TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...) [IF NOT EXISTS]] +select_statement1 FROM from_statement; + +INSERT INTO TABLE tablename1 [PARTITION (partcol1=val1, partcol2=val2 ...)] +select_statement1 FROM from_statement; +``` + ++ Hive 0.13.0 开始,建表时可以通过使用 TBLPROPERTIES(“immutable”=“true”)来创建不可变表 (immutable table) ,如果不可以变表中存在数据,则 INSERT INTO 失败。(注:INSERT OVERWRITE 的语句不受 `immutable` 属性的影响); + ++ 可以对表或分区执行插入操作。如果表已分区,则必须通过指定所有分区列的值来指定表的特定分区; + ++ 从 Hive 1.1.0 开始,TABLE 关键字是可选的; + ++ 从 Hive 1.2.0 开始 ,可以采用 INSERT INTO tablename(z,x,c1) 指明插入列; + ++ 可以将 SELECT 语句的查询结果插入多个表(或分区),称为多表插入。语法如下: + + ```sql + FROM from_statement + INSERT OVERWRITE TABLE tablename1 + [PARTITION (partcol1=val1, partcol2=val2 ...) [IF NOT EXISTS]] select_statement1 + [INSERT OVERWRITE TABLE tablename2 [PARTITION ... [IF NOT EXISTS]] select_statement2] + [INSERT INTO TABLE tablename2 [PARTITION ...] select_statement2] ...; + ``` + +### 2.2 动态插入分区 + +```sql +INSERT OVERWRITE TABLE tablename PARTITION (partcol1[=val1], partcol2[=val2] ...) +select_statement FROM from_statement; + +INSERT INTO TABLE tablename PARTITION (partcol1[=val1], partcol2[=val2] ...) +select_statement FROM from_statement; +``` + +在向分区表插入数据时候,分区列名是必须的,但是列值是可选的。如果给出了分区列值,我们将其称为静态分区,否则它是动态分区。动态分区列必须在 SELECT 语句的列中最后指定,并且与它们在 PARTITION() 子句中出现的顺序相同。 + +注意:Hive 0.9.0 之前的版本动态分区插入是默认禁用的,而 0.9.0 之后的版本则默认启用。以下是动态分区的相关配置: + +| 配置 | 默认值 | 说明 | +| ------------------------------------------ | -------- | ------------------------------------------------------------ | +| `hive.exec.dynamic.partition` | `true` | 需要设置为 true 才能启用动态分区插入 | +| `hive.exec.dynamic.partition.mode` | `strict` | 在严格模式 (strict) 下,用户必须至少指定一个静态分区,以防用户意外覆盖所有分区,在非严格模式下,允许所有分区都是动态的 | +| `hive.exec.max.dynamic.partitions.pernode` | 100 | 允许在每个 mapper/reducer 节点中创建的最大动态分区数 | +| `hive.exec.max.dynamic.partitions` | 1000 | 允许总共创建的最大动态分区数 | +| `hive.exec.max.created.files` | 100000 | 作业中所有 mapper/reducer 创建的 HDFS 文件的最大数量 | +| `hive.error.on.empty.partition` | `false` | 如果动态分区插入生成空结果,是否抛出异常 | + +### 2.3 示例 + +1. 新建 emp 表,作为查询对象表 + +```sql +CREATE TABLE emp( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2), + deptno INT) + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; + + -- 加载数据到 emp 表中 这里直接从本地加载 +load data local inpath "/usr/file/emp.txt" into table emp; +``` +​ 完成后 `emp` 表中数据如下: +
+ +2. 为清晰演示,先清空 `emp_ptn` 表中加载的数据: + +```sql +TRUNCATE TABLE emp_ptn; +``` + +3. 静态分区演示:从 `emp` 表中查询部门编号为 20 的员工数据,并插入 `emp_ptn` 表中,语句如下: + +```sql +INSERT OVERWRITE TABLE emp_ptn PARTITION (deptno=20) +SELECT empno,ename,job,mgr,hiredate,sal,comm FROM emp WHERE deptno=20; +``` + +​ 完成后 `emp_ptn` 表中数据如下: + +
+ +4. 接着演示动态分区: + +```sql +-- 由于我们只有一个分区,且还是动态分区,所以需要关闭严格默认。因为在严格模式下,用户必须至少指定一个静态分区 +set hive.exec.dynamic.partition.mode=nonstrict; + +-- 动态分区 此时查询语句的最后一列为动态分区列,即 deptno +INSERT OVERWRITE TABLE emp_ptn PARTITION (deptno) +SELECT empno,ename,job,mgr,hiredate,sal,comm,deptno FROM emp WHERE deptno=30; +``` + +​ 完成后 `emp_ptn` 表中数据如下: + +
+ + + +## 三、使用SQL语句插入值 + +```sql +INSERT INTO TABLE tablename [PARTITION (partcol1[=val1], partcol2[=val2] ...)] +VALUES ( value [, value ...] ) +``` + ++ 使用时必须为表中的每个列都提供值。不支持只向部分列插入值(可以为缺省值的列提供空值来消除这个弊端); ++ 如果目标表表支持 ACID 及其事务管理器,则插入后自动提交; ++ 不支持支持复杂类型 (array, map, struct, union) 的插入。 + + + +## 四、更新和删除数据 + +### 4.1 语法 + +更新和删除的语法比较简单,和关系型数据库一致。需要注意的是这两个操作都只能在支持 ACID 的表,也就是事务表上才能执行。 + +```sql +-- 更新 +UPDATE tablename SET column = value [, column = value ...] [WHERE expression] + +--删除 +DELETE FROM tablename [WHERE expression] +``` + +### 4.2 示例 + +**1. 修改配置** + +首先需要更改 `hive-site.xml`,添加如下配置,开启事务支持,配置完成后需要重启 Hive 服务。 + +```xml + + hive.support.concurrency + true + + + hive.enforce.bucketing + true + + + hive.exec.dynamic.partition.mode + nonstrict + + + hive.txn.manager + org.apache.hadoop.hive.ql.lockmgr.DbTxnManager + + + hive.compactor.initiator.on + true + + + hive.in.test + true + +``` + +**2. 创建测试表** + +创建用于测试的事务表,建表时候指定属性 `transactional = true` 则代表该表是事务表。需要注意的是,按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions) 的说明,目前 Hive 中的事务表有以下限制: + ++ 必须是 buckets Table; ++ 仅支持 ORC 文件格式; ++ 不支持 LOAD DATA ...语句。 + +```sql +CREATE TABLE emp_ts( + empno int, + ename String +) +CLUSTERED BY (empno) INTO 2 BUCKETS STORED AS ORC +TBLPROPERTIES ("transactional"="true"); +``` + +**3. 插入测试数据** + +```sql +INSERT INTO TABLE emp_ts VALUES (1,"ming"),(2,"hong"); +``` + +插入数据依靠的是 MapReduce 作业,执行成功后数据如下: + +
+ +**4. 测试更新和删除** + +```sql +--更新数据 +UPDATE emp_ts SET ename = "lan" WHERE empno=1; + +--删除数据 +DELETE FROM emp_ts WHERE empno=2; +``` + +更新和删除数据依靠的也是 MapReduce 作业,执行成功后数据如下: + +
+ + +## 五、查询结果写出到文件系统 + +### 5.1 语法 + +```sql +INSERT OVERWRITE [LOCAL] DIRECTORY directory1 + [ROW FORMAT row_format] [STORED AS file_format] + SELECT ... FROM ... +``` + ++ OVERWRITE 关键字表示输出文件存在时,先删除后再重新写入; + ++ 和 Load 语句一样,建议无论是本地路径还是 URL 地址都使用完整的; + ++ 写入文件系统的数据被序列化为文本,其中列默认由^A 分隔,行由换行符分隔。如果列不是基本类型,则将其序列化为 JSON 格式。其中行分隔符不允许自定义,但列分隔符可以自定义,如下: + + ```sql + -- 定义列分隔符为'\t' + insert overwrite local directory './test-04' + row format delimited + FIELDS TERMINATED BY '\t' + COLLECTION ITEMS TERMINATED BY ',' + MAP KEYS TERMINATED BY ':' + select * from src; + ``` + +### 5.2 示例 + +这里我们将上面创建的 `emp_ptn` 表导出到本地文件系统,语句如下: + +```sql +INSERT OVERWRITE LOCAL DIRECTORY '/usr/file/ouput' +ROW FORMAT DELIMITED +FIELDS TERMINATED BY '\t' +SELECT * FROM emp_ptn; +``` + +导出结果如下: + +
+ + + + + +## 参考资料 + +1. [Hive Transactions](https://cwiki.apache.org/confluence/display/Hive/Hive+Transactions) +2. [Hive Data Manipulation Language](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DML) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\346\225\260\346\215\256\346\237\245\350\257\242\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\346\225\260\346\215\256\346\237\245\350\257\242\350\257\246\350\247\243.md" new file mode 100644 index 0000000..b8c7750 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\346\225\260\346\215\256\346\237\245\350\257\242\350\257\246\350\247\243.md" @@ -0,0 +1,396 @@ +# Hive数据查询详解 + + + + + +## 一、数据准备 + +为了演示查询操作,这里需要预先创建三张表,并加载测试数据。 + +> 数据文件 emp.txt 和 dept.txt 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。 + +### 1.1 员工表 + +```sql + -- 建表语句 + CREATE TABLE emp( + empno INT, -- 员工表编号 + ename STRING, -- 员工姓名 + job STRING, -- 职位类型 + mgr INT, + hiredate TIMESTAMP, --雇佣日期 + sal DECIMAL(7,2), --工资 + comm DECIMAL(7,2), + deptno INT) --部门编号 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; + + --加载数据 +LOAD DATA LOCAL INPATH "/usr/file/emp.txt" OVERWRITE INTO TABLE emp; +``` + +### 1.2 部门表 + +```sql + -- 建表语句 + CREATE TABLE dept( + deptno INT, --部门编号 + dname STRING, --部门名称 + loc STRING --部门所在的城市 + ) + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; + + --加载数据 + LOAD DATA LOCAL INPATH "/usr/file/dept.txt" OVERWRITE INTO TABLE dept; +``` + +### 1.3 分区表 + +这里需要额外创建一张分区表,主要是为了演示分区查询: + +```sql +CREATE EXTERNAL TABLE emp_ptn( + empno INT, + ename STRING, + job STRING, + mgr INT, + hiredate TIMESTAMP, + sal DECIMAL(7,2), + comm DECIMAL(7,2) + ) + PARTITIONED BY (deptno INT) -- 按照部门编号进行分区 + ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; + + +--加载数据 +LOAD DATA LOCAL INPATH "/usr/file/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=20) +LOAD DATA LOCAL INPATH "/usr/file/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=30) +LOAD DATA LOCAL INPATH "/usr/file/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=40) +LOAD DATA LOCAL INPATH "/usr/file/emp.txt" OVERWRITE INTO TABLE emp_ptn PARTITION (deptno=50) +``` + + + +## 二、单表查询 + +### 2.1 SELECT + +```sql +-- 查询表中全部数据 +SELECT * FROM emp; +``` + + + +### 2.2 WHERE + +```sql +-- 查询 10 号部门中员工编号大于 7782 的员工信息 +SELECT * FROM emp WHERE empno > 7782 AND deptno = 10; +``` + + + +### 2.3 DISTINCT + +Hive 支持使用 DISTINCT 关键字去重。 + +```sql +-- 查询所有工作类型 +SELECT DISTINCT job FROM emp; +``` + + + +### 2.4 分区查询 + +分区查询 (Partition Based Queries),可以指定某个分区或者分区范围。 + +```sql +-- 查询分区表中部门编号在[20,40]之间的员工 +SELECT emp_ptn.* FROM emp_ptn +WHERE emp_ptn.deptno >= 20 AND emp_ptn.deptno <= 40; +``` + + + +### 2.5 LIMIT + +```sql +-- 查询薪资最高的 5 名员工 +SELECT * FROM emp ORDER BY sal DESC LIMIT 5; +``` + + + +### 2.6 GROUP BY + +Hive 支持使用 GROUP BY 进行分组聚合操作。 + +```sql +set hive.map.aggr=true; + +-- 查询各个部门薪酬综合 +SELECT deptno,SUM(sal) FROM emp GROUP BY deptno; +``` + +`hive.map.aggr` 控制程序如何进行聚合。默认值为 false。如果设置为 true,Hive 会在 map 阶段就执行一次聚合。这可以提高聚合效率,但需要消耗更多内存。 + + + +### 2.7 ORDER AND SORT + +可以使用 ORDER BY 或者 Sort BY 对查询结果进行排序,排序字段可以是整型也可以是字符串:如果是整型,则按照大小排序;如果是字符串,则按照字典序排序。ORDER BY 和 SORT BY 的区别如下: + ++ 使用 ORDER BY 时会有一个 Reducer 对全部查询结果进行排序,可以保证数据的全局有序性; ++ 使用 SORT BY 时只会在每个 Reducer 中进行排序,这可以保证每个 Reducer 的输出数据是有序的,但不能保证全局有序。 + +由于 ORDER BY 的时间可能很长,如果你设置了严格模式 (hive.mapred.mode = strict),则其后面必须再跟一个 `limit` 子句。 + +> 注 :hive.mapred.mode 默认值是 nonstrict ,也就是非严格模式。 + +```sql +-- 查询员工工资,结果按照部门升序,按照工资降序排列 +SELECT empno, deptno, sal FROM emp ORDER BY deptno ASC, sal DESC; +``` + + + +### 2.8 HAVING + +可以使用 HAVING 对分组数据进行过滤。 + +```sql +-- 查询工资总和大于 9000 的所有部门 +SELECT deptno,SUM(sal) FROM emp GROUP BY deptno HAVING SUM(sal)>9000; +``` + + + +### 2.9 DISTRIBUTE BY + +默认情况下,MapReduce 程序会对 Map 输出结果的 Key 值进行散列,并均匀分发到所有 Reducer 上。如果想要把具有相同 Key 值的数据分发到同一个 Reducer 进行处理,这就需要使用 DISTRIBUTE BY 字句。 + +需要注意的是,DISTRIBUTE BY 虽然能保证具有相同 Key 值的数据分发到同一个 Reducer,但是不能保证数据在 Reducer 上是有序的。情况如下: + +把以下 5 个数据发送到两个 Reducer 上进行处理: + +```properties +k1 +k2 +k4 +k3 +k1 +``` + +Reducer1 得到如下乱序数据: + +```properties +k1 +k2 +k1 +``` + + +Reducer2 得到数据如下: + +```properties +k4 +k3 +``` + +如果想让 Reducer 上的数据时有序的,可以结合 `SORT BY` 使用 (示例如下),或者使用下面我们将要介绍的 CLUSTER BY。 + +```sql +-- 将数据按照部门分发到对应的 Reducer 上处理 +SELECT empno, deptno, sal FROM emp DISTRIBUTE BY deptno SORT BY deptno ASC; +``` + + + +### 2.10 CLUSTER BY + +如果 `SORT BY` 和 `DISTRIBUTE BY` 指定的是相同字段,且 SORT BY 排序规则是 ASC,此时可以使用 `CLUSTER BY` 进行替换,同时 `CLUSTER BY` 可以保证数据在全局是有序的。 + +```sql +SELECT empno, deptno, sal FROM emp CLUSTER BY deptno ; +``` + + + +## 三、多表联结查询 + +Hive 支持内连接,外连接,左外连接,右外连接,笛卡尔连接,这和传统数据库中的概念是一致的,可以参见下图。 + +需要特别强调:JOIN 语句的关联条件必须用 ON 指定,不能用 WHERE 指定,否则就会先做笛卡尔积,再过滤,这会导致你得不到预期的结果 (下面的演示会有说明)。 + +
+ +### 3.1 INNER JOIN + +```sql +-- 查询员工编号为 7369 的员工的详细信息 +SELECT e.*,d.* FROM +emp e JOIN dept d +ON e.deptno = d.deptno +WHERE empno=7369; + +--如果是三表或者更多表连接,语法如下 +SELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key1) JOIN c ON (c.key = b.key1) +``` + +### 3.2 LEFT OUTER JOIN + +LEFT OUTER JOIN 和 LEFT JOIN 是等价的。 + +```sql +-- 左连接 +SELECT e.*,d.* +FROM emp e LEFT OUTER JOIN dept d +ON e.deptno = d.deptno; +``` + +### 3.3 RIGHT OUTER JOIN + +```sql +--右连接 +SELECT e.*,d.* +FROM emp e RIGHT OUTER JOIN dept d +ON e.deptno = d.deptno; +``` + +执行右连接后,由于 40 号部门下没有任何员工,所以此时员工信息为 NULL。这个查询可以很好的复述上面提到的——JOIN 语句的关联条件必须用 ON 指定,不能用 WHERE 指定。你可以把 ON 改成 WHERE,你会发现无论如何都查不出 40 号部门这条数据,因为笛卡尔运算不会有 (NULL, 40) 这种情况。 + +
+### 3.4 FULL OUTER JOIN + +```sql +SELECT e.*,d.* +FROM emp e FULL OUTER JOIN dept d +ON e.deptno = d.deptno; +``` + +### 3.5 LEFT SEMI JOIN + +LEFT SEMI JOIN (左半连接)是 IN/EXISTS 子查询的一种更高效的实现。 + ++ JOIN 子句中右边的表只能在 ON 子句中设置过滤条件; ++ 查询结果只包含左边表的数据,所以只能 SELECT 左表中的列。 + +```sql +-- 查询在纽约办公的所有员工信息 +SELECT emp.* +FROM emp LEFT SEMI JOIN dept +ON emp.deptno = dept.deptno AND dept.loc="NEW YORK"; + +--上面的语句就等价于 +SELECT emp.* FROM emp +WHERE emp.deptno IN (SELECT deptno FROM dept WHERE loc="NEW YORK"); +``` + +### 3.6 JOIN + +笛卡尔积连接,这个连接日常的开发中可能很少遇到,且性能消耗比较大,基于这个原因,如果在严格模式下 (hive.mapred.mode = strict),Hive 会阻止用户执行此操作。 + +```sql +SELECT * FROM emp JOIN dept; +``` + + + +## 四、JOIN优化 + +### 4.1 STREAMTABLE + +在多表进行联结的时候,如果每个 ON 字句都使用到共同的列(如下面的 `b.key`),此时 Hive 会进行优化,将多表 JOIN 在同一个 map / reduce 作业上进行。同时假定查询的最后一个表(如下面的 c 表)是最大的一个表,在对每行记录进行 JOIN 操作时,它将尝试将其他的表缓存起来,然后扫描最后那个表进行计算。因此用户需要保证查询的表的大小从左到右是依次增加的。 + +```sql +`SELECT a.val, b.val, c.val FROM a JOIN b ON (a.key = b.key) JOIN c ON (c.key = b.key)` +``` + +然后,用户并非需要总是把最大的表放在查询语句的最后面,Hive 提供了 `/*+ STREAMTABLE() */` 标志,用于标识最大的表,示例如下: + +```sql +SELECT /*+ STREAMTABLE(d) */ e.*,d.* +FROM emp e JOIN dept d +ON e.deptno = d.deptno +WHERE job='CLERK'; +``` + + + +### 4.2 MAPJOIN + +如果所有表中只有一张表是小表,那么 Hive 把这张小表加载到内存中。这时候程序会在 map 阶段直接拿另外一个表的数据和内存中表数据做匹配,由于在 map 就进行了 JOIN 操作,从而可以省略 reduce 过程,这样效率可以提升很多。Hive 中提供了 `/*+ MAPJOIN() */` 来标记小表,示例如下: + +```sql +SELECT /*+ MAPJOIN(d) */ e.*,d.* +FROM emp e JOIN dept d +ON e.deptno = d.deptno +WHERE job='CLERK'; +``` + + + +## 五、SELECT的其他用途 + +查看当前数据库: + +```sql +SELECT current_database() +``` + + + +## 六、本地模式 + +在上面演示的语句中,大多数都会触发 MapReduce, 少部分不会触发,比如 `select * from emp limit 5` 就不会触发 MR,此时 Hive 只是简单的读取数据文件中的内容,然后格式化后进行输出。在需要执行 MapReduce 的查询中,你会发现执行时间可能会很长,这时候你可以选择开启本地模式。 + +```sql +--本地模式默认关闭,需要手动开启此功能 +SET hive.exec.mode.local.auto=true; +``` + +启用后,Hive 将分析查询中每个 map-reduce 作业的大小,如果满足以下条件,则可以在本地运行它: + +- 作业的总输入大小低于:hive.exec.mode.local.auto.inputbytes.max(默认为 128MB); +- map-tasks 的总数小于:hive.exec.mode.local.auto.tasks.max(默认为 4); +- 所需的 reduce 任务总数为 1 或 0。 + +因为我们测试的数据集很小,所以你再次去执行上面涉及 MR 操作的查询,你会发现速度会有显著的提升。 + + + + + +## 参考资料 + +1. [LanguageManual Select](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Select) +2. [LanguageManual Joins](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Joins) +3. [LanguageManual GroupBy](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+GroupBy) +4. [LanguageManual SortBy](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+SortBy) \ No newline at end of file diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" new file mode 100644 index 0000000..63c49f4 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" @@ -0,0 +1,202 @@ +# Hive简介及核心概念 + + + + +## 一、简介 + +Hive 是一个构建在 Hadoop 之上的数据仓库,它可以将结构化的数据文件映射成表,并提供类 SQL 查询功能,用于查询的 SQL 语句会被转化为 MapReduce 作业,然后提交到 Hadoop 上运行。 + +**特点**: + +1. 简单、容易上手 (提供了类似 sql 的查询语言 hql),使得精通 sql 但是不了解 Java 编程的人也能很好地进行大数据分析; +3. 灵活性高,可以自定义用户函数 (UDF) 和存储格式; +4. 为超大的数据集设计的计算和存储能力,集群扩展容易; +5. 统一的元数据管理,可与 presto/impala/sparksql 等共享数据; +5. 执行延迟高,不适合做数据的实时处理,但适合做海量数据的离线处理。 + + + +## 二、Hive的体系架构 + +
+ +### 2.1 command-line shell & thrift/jdbc + +可以用 command-line shell 和 thrift/jdbc 两种方式来操作数据: + ++ **command-line shell**:通过 hive 命令行的的方式来操作数据; ++ **thrift/jdbc**:通过 thrift 协议按照标准的 JDBC 的方式操作数据。 + +### 2.2 Metastore + +在 Hive 中,表名、表结构、字段名、字段类型、表的分隔符等统一被称为元数据。所有的元数据默认存储在 Hive 内置的 derby 数据库中,但由于 derby 只能有一个实例,也就是说不能有多个命令行客户端同时访问,所以在实际生产环境中,通常使用 MySQL 代替 derby。 + +Hive 进行的是统一的元数据管理,就是说你在 Hive 上创建了一张表,然后在 presto/impala/sparksql 中都是可以直接使用的,它们会从 Metastore 中获取统一的元数据信息,同样的你在 presto/impala/sparksql 中创建一张表,在 Hive 中也可以直接使用。 + +### 2.3 HQL的执行流程 + +Hive 在执行一条 HQL 的时候,会经过以下步骤: + +1. 语法解析:Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象 语法树 AST Tree; +2. 语义解析:遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock; +3. 生成逻辑执行计划:遍历 QueryBlock,翻译为执行操作树 OperatorTree; +4. 优化逻辑执行计划:逻辑层优化器进行 OperatorTree 变换,合并不必要的 ReduceSinkOperator,减少 shuffle 数据量; +5. 生成物理执行计划:遍历 OperatorTree,翻译为 MapReduce 任务; +6. 优化物理执行计划:物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划。 + +> 关于 Hive SQL 的详细执行流程可以参考美团技术团队的文章:[Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) + + + +## 三、数据类型 + +### 3.1 基本数据类型 + +Hive 表中的列支持以下基本数据类型: + +| 大类 | 类型 | +| --------------------------------------- | ------------------------------------------------------------ | +| **Integers(整型)** | TINYINT—1 字节的有符号整数
SMALLINT—2 字节的有符号整数
INT—4 字节的有符号整数
BIGINT—8 字节的有符号整数 | +| **Boolean(布尔型)** | BOOLEAN—TRUE/FALSE | +| **Floating point numbers(浮点型)** | FLOAT— 单精度浮点型
DOUBLE—双精度浮点型 | +| **Fixed point numbers(定点数)** | DECIMAL—用户自定义精度定点数,比如 DECIMAL(7,2) | +| **String types(字符串)** | STRING—指定字符集的字符序列
VARCHAR—具有最大长度限制的字符序列
CHAR—固定长度的字符序列 | +| **Date and time types(日期时间类型)** | TIMESTAMP — 时间戳
TIMESTAMP WITH LOCAL TIME ZONE — 时间戳,纳秒精度
DATE—日期类型 | +| **Binary types(二进制类型)** | BINARY—字节序列 | + +> TIMESTAMP 和 TIMESTAMP WITH LOCAL TIME ZONE 的区别如下: +> +> - **TIMESTAMP WITH LOCAL TIME ZONE**:用户提交时间给数据库时,会被转换成数据库所在的时区来保存。查询时则按照查询客户端的不同,转换为查询客户端所在时区的时间。 +> - **TIMESTAMP** :提交什么时间就保存什么时间,查询时也不做任何转换。 + +### 3.2 隐式转换 + +Hive 中基本数据类型遵循以下的层次结构,按照这个层次结构,子类型到祖先类型允许隐式转换。例如 INT 类型的数据允许隐式转换为 BIGINT 类型。额外注意的是:按照类型层次结构允许将 STRING 类型隐式转换为 DOUBLE 类型。 + +
+ + + +### 3.3 复杂类型 + +| 类型 | 描述 | 示例 | +| ---------- | ------------------------------------------------------------ | -------------------------------------- | +| **STRUCT** | 类似于对象,是字段的集合,字段的类型可以不同,可以使用 ` 名称.字段名 ` 方式进行访问 | STRUCT ('xiaoming', 12 , '2018-12-12') | +| **MAP** | 键值对的集合,可以使用 ` 名称[key]` 的方式访问对应的值 | map('a', 1, 'b', 2) | +| **ARRAY** | 数组是一组具有相同类型和名称的变量的集合,可以使用 ` 名称[index]` 访问对应的值 | ARRAY('a', 'b', 'c', 'd') | + + + +### 3.4 示例 + +如下给出一个基本数据类型和复杂数据类型的使用示例: + +```sql +CREATE TABLE students( + name STRING, -- 姓名 + age INT, -- 年龄 + subject ARRAY, --学科 + score MAP, --各个学科考试成绩 + address STRUCT --家庭居住地址 +) ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"; +``` + + + +## 四、内容格式 + +当数据存储在文本文件中,必须按照一定格式区别行和列,如使用逗号作为分隔符的 CSV 文件 (Comma-Separated Values) 或者使用制表符作为分隔值的 TSV 文件 (Tab-Separated Values)。但此时也存在一个缺点,就是正常的文件内容中也可能出现逗号或者制表符。 + +所以 Hive 默认使用了几个平时很少出现的字符,这些字符一般不会作为内容出现在文件中。Hive 默认的行和列分隔符如下表所示。 + +| 分隔符 | 描述 | +| --------------- | ------------------------------------------------------------ | +| **\n** | 对于文本文件来说,每行是一条记录,所以可以使用换行符来分割记录 | +| **^A (Ctrl+A)** | 分割字段 (列),在 CREATE TABLE 语句中也可以使用八进制编码 `\001` 来表示 | +| **^B** | 用于分割 ARRAY 或者 STRUCT 中的元素,或者用于 MAP 中键值对之间的分割,
在 CREATE TABLE 语句中也可以使用八进制编码 `\002` 表示 | +| **^C** | 用于 MAP 中键和值之间的分割,在 CREATE TABLE 语句中也可以使用八进制编码 `\003` 表示 | + +使用示例如下: + +```sql +CREATE TABLE page_view(viewTime INT, userid BIGINT) + ROW FORMAT DELIMITED + FIELDS TERMINATED BY '\001' + COLLECTION ITEMS TERMINATED BY '\002' + MAP KEYS TERMINATED BY '\003' + STORED AS SEQUENCEFILE; +``` + + + +## 五、存储格式 + +### 5.1 支持的存储格式 + +Hive 会在 HDFS 为每个数据库上创建一个目录,数据库中的表是该目录的子目录,表中的数据会以文件的形式存储在对应的表目录下。Hive 支持以下几种文件存储格式: + +| 格式 | 说明 | +| ---------------- | ------------------------------------------------------------ | +| **TextFile** | 存储为纯文本文件。 这是 Hive 默认的文件存储格式。这种存储方式数据不做压缩,磁盘开销大,数据解析开销大。 | +| **SequenceFile** | SequenceFile 是 Hadoop API 提供的一种二进制文件,它将数据以的形式序列化到文件中。这种二进制文件内部使用 Hadoop 的标准的 Writable 接口实现序列化和反序列化。它与 Hadoop API 中的 MapFile 是互相兼容的。Hive 中的 SequenceFile 继承自 Hadoop API 的 SequenceFile,不过它的 key 为空,使用 value 存放实际的值,这样是为了避免 MR 在运行 map 阶段进行额外的排序操作。 | +| **RCFile** | RCFile 文件格式是 FaceBook 开源的一种 Hive 的文件存储格式,首先将表分为几个行组,对每个行组内的数据按列存储,每一列的数据都是分开存储。 | +| **ORC Files** | ORC 是在一定程度上扩展了 RCFile,是对 RCFile 的优化。 | +| **Avro Files** | Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动态语言可以方便地处理 Avro 数据。 | +| **Parquet** | Parquet 是基于 Dremel 的数据模型和算法实现的,面向分析型业务的列式存储格式。它通过按列进行高效压缩和特殊的编码技术,从而在降低存储空间的同时提高了 IO 效率。 | + +> 以上压缩格式中 ORC 和 Parquet 的综合性能突出,使用较为广泛,推荐使用这两种格式。 + +### 5.2 指定存储格式 + +通常在创建表的时候使用 `STORED AS` 参数指定: + +```sql +CREATE TABLE page_view(viewTime INT, userid BIGINT) + ROW FORMAT DELIMITED + FIELDS TERMINATED BY '\001' + COLLECTION ITEMS TERMINATED BY '\002' + MAP KEYS TERMINATED BY '\003' + STORED AS SEQUENCEFILE; +``` + +各个存储文件类型指定方式如下: + +- STORED AS TEXTFILE +- STORED AS SEQUENCEFILE +- STORED AS ORC +- STORED AS PARQUET +- STORED AS AVRO +- STORED AS RCFILE + + + +## 六、内部表和外部表 + +内部表又叫做管理表 (Managed/Internal Table),创建表时不做任何指定,默认创建的就是内部表。想要创建外部表 (External Table),则需要使用 External 进行修饰。 内部表和外部表主要区别如下: + +| | 内部表 | 外部表 | +| ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 数据存储位置 | 内部表数据存储的位置由 hive.metastore.warehouse.dir 参数指定,默认情况下表的数据存储在 HDFS 的 `/user/hive/warehouse/数据库名.db/表名/` 目录下 | 外部表数据的存储位置创建表时由 `Location` 参数指定; | +| 导入数据 | 在导入数据到内部表,内部表将数据移动到自己的数据仓库目录下,数据的生命周期由 Hive 来进行管理 | 外部表不会将数据移动到自己的数据仓库目录下,只是在元数据中存储了数据的位置 | +| 删除表 | 删除元数据(metadata)和文件 | 只删除元数据(metadata) | + + + +## 参考资料 + +1. [Hive Getting Started](https://cwiki.apache.org/confluence/display/Hive/GettingStarted) +2. [Hive SQL 的编译过程](https://tech.meituan.com/2014/02/12/hive-sql-to-mapreduce.html) +3. [LanguageManual DDL](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL) +4. [LanguageManual Types](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types) +5. [Managed vs. External Tables](https://cwiki.apache.org/confluence/display/Hive/Managed+vs.+External+Tables) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" new file mode 100644 index 0000000..c2fd9a0 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" @@ -0,0 +1,236 @@ +# Hive 视图和索引 + + + +## 一、视图 + +### 1.1 简介 + +Hive 中的视图和 RDBMS 中视图的概念一致,都是一组数据的逻辑表示,本质上就是一条 SELECT 语句的结果集。视图是纯粹的逻辑对象,没有关联的存储 (Hive 3.0.0 引入的物化视图除外),当查询引用视图时,Hive 可以将视图的定义与查询结合起来,例如将查询中的过滤器推送到视图中。 + +### 1.2 创建视图 + +```sql +CREATE VIEW [IF NOT EXISTS] [db_name.]view_name -- 视图名称 + [(column_name [COMMENT column_comment], ...) ] --列名 + [COMMENT view_comment] --视图注释 + [TBLPROPERTIES (property_name = property_value, ...)] --额外信息 + AS SELECT ...; +``` + +在 Hive 中可以使用 `CREATE VIEW` 创建视图,如果已存在具有相同名称的表或视图,则会抛出异常,建议使用 `IF NOT EXISTS` 预做判断。在使用视图时候需要注意以下事项: + +- 视图是只读的,不能用作 LOAD / INSERT / ALTER 的目标; + +- 在创建视图时候视图就已经固定,对基表的后续更改(如添加列)将不会反映在视图; + +- 删除基表并不会删除视图,需要手动删除视图; + +- 视图可能包含 ORDER BY 和 LIMIT 子句。如果引用视图的查询语句也包含这类子句,其执行优先级低于视图对应字句。例如,视图 `custom_view` 指定 LIMIT 5,查询语句为 `select * from custom_view LIMIT 10`,此时结果最多返回 5 行。 + +- 创建视图时,如果未提供列名,则将从 SELECT 语句中自动派生列名; + +- 创建视图时,如果 SELECT 语句中包含其他表达式,例如 x + y,则列名称将以\_C0,\_C1 等形式生成; + + ```sql + CREATE VIEW IF NOT EXISTS custom_view AS SELECT empno, empno+deptno , 1+2 FROM emp; + ``` + +
+ + + +### 1.3 查看视图 + +```sql +-- 查看所有视图: 没有单独查看视图列表的语句,只能使用 show tables +show tables; +-- 查看某个视图 +desc view_name; +-- 查看某个视图详细信息 +desc formatted view_name; +``` + + + +### 1.4 删除视图 + +```sql +DROP VIEW [IF EXISTS] [db_name.]view_name; +``` + +删除视图时,如果被删除的视图被其他视图所引用,这时候程序不会发出警告,但是引用该视图其他视图已经失效,需要进行重建或者删除。 + + + +### 1.5 修改视图 + +```sql +ALTER VIEW [db_name.]view_name AS select_statement; +``` + + 被更改的视图必须存在,且视图不能具有分区,如果视图具有分区,则修改失败。 + + + +### 1.6 修改视图属性 + +语法: + +```sql +ALTER VIEW [db_name.]view_name SET TBLPROPERTIES table_properties; + +table_properties: + : (property_name = property_value, property_name = property_value, ...) +``` + +示例: + +```sql +ALTER VIEW custom_view SET TBLPROPERTIES ('create'='heibaiying','date'='2019-05-05'); +``` + +
+ + + + + +## 二、索引 + +### 2.1 简介 + +Hive 在 0.7.0 引入了索引的功能,索引的设计目标是提高表某些列的查询速度。如果没有索引,带有谓词的查询(如'WHERE table1.column = 10')会加载整个表或分区并处理所有行。但是如果 column 存在索引,则只需要加载和处理文件的一部分。 + +### 2.2 索引原理 + +在指定列上建立索引,会产生一张索引表(表结构如下),里面的字段包括:索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。在查询涉及到索引字段时,首先到索引表查找索引列值对应的 HDFS 文件路径及偏移量,这样就避免了全表扫描。 + +```properties ++--------------+----------------+----------+--+ +| col_name | data_type | comment | ++--------------+----------------+----------+--+ +| empno | int | 建立索引的列 | +| _bucketname | string | HDFS 文件路径 | +| _offsets | array | 偏移量 | ++--------------+----------------+----------+--+ +``` + +### 2.3 创建索引 + +```sql +CREATE INDEX index_name --索引名称 + ON TABLE base_table_name (col_name, ...) --建立索引的列 + AS index_type --索引类型 + [WITH DEFERRED REBUILD] --重建索引 + [IDXPROPERTIES (property_name=property_value, ...)] --索引额外属性 + [IN TABLE index_table_name] --索引表的名字 + [ + [ ROW FORMAT ...] STORED AS ... + | STORED BY ... + ] --索引表行分隔符 、 存储格式 + [LOCATION hdfs_path] --索引表存储位置 + [TBLPROPERTIES (...)] --索引表表属性 + [COMMENT "index comment"]; --索引注释 +``` + +### 2.4 查看索引 + +```sql +--显示表上所有列的索引 +SHOW FORMATTED INDEX ON table_name; +``` + +### 2.4 删除索引 + +删除索引会删除对应的索引表。 + +```sql +DROP INDEX [IF EXISTS] index_name ON table_name; +``` + +如果存在索引的表被删除了,其对应的索引和索引表都会被删除。如果被索引表的某个分区被删除了,那么分区对应的分区索引也会被删除。 + +### 2.5 重建索引 + +```sql +ALTER INDEX index_name ON table_name [PARTITION partition_spec] REBUILD; +``` + +重建索引。如果指定了 PARTITION,则仅重建该分区的索引。 + + + +## 三、索引案例 + +### 3.1 创建索引 + +在 emp 表上针对 `empno` 字段创建名为 `emp_index`,索引数据存储在 `emp_index_table` 索引表中 + +```sql +create index emp_index on table emp(empno) as +'org.apache.hadoop.hive.ql.index.compact.CompactIndexHandler' +with deferred rebuild +in table emp_index_table ; +``` + +此时索引表中是没有数据的,需要重建索引才会有索引的数据。 + +### 3.2 重建索引 + +```sql +alter index emp_index on emp rebuild; +``` + +Hive 会启动 MapReduce 作业去建立索引,建立好后查看索引表数据如下。三个表字段分别代表:索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。 + +
+ +### 3.3 自动使用索引 + +默认情况下,虽然建立了索引,但是 Hive 在查询时候是不会自动去使用索引的,需要开启相关配置。开启配置后,涉及到索引列的查询就会使用索引功能去优化查询。 + +```sql +SET hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; +SET hive.optimize.index.filter=true; +SET hive.optimize.index.filter.compact.minsize=0; +``` + +### 3.4 查看索引 + +```sql +SHOW INDEX ON emp; +``` + +
+ + + + + +## 四、索引的缺陷 + +索引表最主要的一个缺陷在于:索引表无法自动 rebuild,这也就意味着如果表中有数据新增或删除,则必须手动 rebuild,重新执行 MapReduce 作业,生成索引表数据。 + +同时按照[官方文档](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing) 的说明,Hive 会从 3.0 开始移除索引功能,主要基于以下两个原因: + +- 具有自动重写的物化视图 (Materialized View) 可以产生与索引相似的效果(Hive 2.3.0 增加了对物化视图的支持,在 3.0 之后正式引入)。 +- 使用列式存储文件格式(Parquet,ORC)进行存储时,这些格式支持选择性扫描,可以跳过不需要的文件或块。 + +> ORC 内置的索引功能可以参阅这篇文章:[Hive 性能优化之 ORC 索引–Row Group Index vs Bloom Filter Index](http://lxw1234.com/archives/2016/04/632.htm) + + + + + +## 参考资料 + +1. [Create/Drop/Alter View](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+DDL#LanguageManualDDL-Create/Drop/AlterView) +2. [Materialized views](https://cwiki.apache.org/confluence/display/Hive/Materialized+views) +3. [Hive 索引](http://lxw1234.com/archives/2015/05/207.htm) +4. [Overview of Hive Indexes](https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Indexing) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\266\210\350\264\271\350\200\205\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\266\210\350\264\271\350\200\205\350\257\246\350\247\243.md" new file mode 100644 index 0000000..ea9fb2c --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\266\210\350\264\271\350\200\205\350\257\246\350\247\243.md" @@ -0,0 +1,392 @@ +# Kafka消费者详解 + + + + +## 一、消费者和消费者群组 + +在 Kafka 中,消费者通常是消费者群组的一部分,多个消费者群组共同读取同一个主题时,彼此之间互不影响。Kafka 之所以要引入消费者群组这个概念是因为 Kafka 消费者经常会做一些高延迟的操作,比如把数据写到数据库或 HDFS ,或者进行耗时的计算,在这些情况下,单个消费者无法跟上数据生成的速度。此时可以增加更多的消费者,让它们分担负载,分别处理部分分区的消息,这就是 Kafka 实现横向伸缩的主要手段。 + +
+ +需要注意的是:同一个分区只能被同一个消费者群组里面的一个消费者读取,不可能存在同一个分区被同一个消费者群里多个消费者共同读取的情况,如图: + +
+ +可以看到即便消费者 Consumer5 空闲了,但是也不会去读取任何一个分区的数据,这同时也提醒我们在使用时应该合理设置消费者的数量,以免造成闲置和额外开销。 + +## 二、分区再均衡 + +因为群组里的消费者共同读取主题的分区,所以当一个消费者被关闭或发生崩溃时,它就离开了群组,原本由它读取的分区将由群组里的其他消费者来读取。同时在主题发生变化时 , 比如添加了新的分区,也会发生分区与消费者的重新分配,分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。正是因为再均衡,所以消费费者群组才能保证高可用性和伸缩性。 + +消费者通过向群组协调器所在的 broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发再均衡。 + + +## 三、创建Kafka消费者 + +在创建消费者的时候以下以下三个选项是必选的: + +- **bootstrap.servers** :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错; +- **key.deserializer** :指定键的反序列化器; +- **value.deserializer** :指定值的反序列化器。 + +除此之外你还需要指明你需要想订阅的主题,可以使用如下两个 API : + ++ **consumer.subscribe(Collection\ topics)** :指明需要订阅的主题的集合; ++ **consumer.subscribe(Pattern pattern)** :使用正则来匹配需要订阅的集合。 + +最后只需要通过轮询 API(`poll`) 向服务器定时请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据,这使得开发者只需要关注从分区返回的数据,然后进行业务处理。 示例如下: + +```scala +String topic = "Hello-Kafka"; +String group = "group1"; +Properties props = new Properties(); +props.put("bootstrap.servers", "hadoop001:9092"); +/*指定分组 ID*/ +props.put("group.id", group); +props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); +props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); +KafkaConsumer consumer = new KafkaConsumer<>(props); + +/*订阅主题 (s)*/ +consumer.subscribe(Collections.singletonList(topic)); + +try { + while (true) { + /*轮询获取数据*/ + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.printf("topic = %s,partition = %d, key = %s, value = %s, offset = %d,\n", + record.topic(), record.partition(), record.key(), record.value(), record.offset()); + } + } +} finally { + consumer.close(); +} +``` + +> 本篇文章的所有示例代码可以从 Github 上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) + +## 三、 自动提交偏移量 + +### 3.1 偏移量的重要性 + +Kafka 的每一条消息都有一个偏移量属性,记录了其在分区中的位置,偏移量是一个单调递增的整数。消费者通过往一个叫作 `_consumer_offset` 的特殊主题发送消息,消息里包含每个分区的偏移量。 如果消费者一直处于运行状态,那么偏移量就没有 +什么用处。不过,如果有消费者退出或者新分区加入,此时就会触发再均衡。完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。 因为这个原因,所以如果不能正确提交偏移量,就可能会导致数据丢失或者重复出现消费,比如下面情况: + ++ 如果提交的偏移量小于客户端处理的最后一个消息的偏移量 ,那么处于两个偏移量之间的消息就会被重复消费; ++ 如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失。 + +### 3.2 自动提交偏移量 + +Kafka 支持自动提交和手动提交偏移量两种方式。这里先介绍比较简单的自动提交: + +只需要将消费者的 `enable.auto.commit` 属性配置为 `true` 即可完成自动提交的配置。 此时每隔固定的时间,消费者就会把 `poll()` 方法接收到的最大偏移量进行提交,提交间隔由 `auto.commit.interval.ms` 属性进行配置,默认值是 5s。 + +使用自动提交是存在隐患的,假设我们使用默认的 5s 提交时间间隔,在最近一次提交之后的 3s 发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后了 3s ,所以在这 3s 内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复消息的时间窗,不过这种情况是无法完全避免的。基于这个原因,Kafka 也提供了手动提交偏移量的 API,使得用户可以更为灵活的提交偏移量。 + + + +## 四、手动提交偏移量 + +用户可以通过将 `enable.auto.commit` 设为 `false`,然后手动提交偏移量。基于用户需求手动提交偏移量可以分为两大类: + ++ 手动提交当前偏移量:即手动提交当前轮询的最大偏移量; ++ 手动提交固定偏移量:即按照业务需求,提交某一个固定的偏移量。 + +而按照 Kafka API,手动提交偏移量又可以分为同步提交和异步提交。 + +### 4.1 同步提交 + +通过调用 `consumer.commitSync()` 来进行同步提交,不传递任何参数时提交的是当前轮询的最大偏移量。 + +```java +while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.println(record); + } + /*同步提交*/ + consumer.commitSync(); +} +``` + +如果某个提交失败,同步提交还会进行重试,这可以保证数据能够最大限度提交成功,但是同时也会降低程序的吞吐量。基于这个原因,Kafka 还提供了异步提交的 API。 + +### 4.2 异步提交 + +异步提交可以提高程序的吞吐量,因为此时你可以尽管请求数据,而不用等待 Broker 的响应。代码如下: + +```java +while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.println(record); + } + /*异步提交并定义回调*/ + consumer.commitAsync(new OffsetCommitCallback() { + @Override + public void onComplete(Map offsets, Exception exception) { + if (exception != null) { + System.out.println("错误处理"); + offsets.forEach((x, y) -> System.out.printf("topic = %s,partition = %d, offset = %s \n", + x.topic(), x.partition(), y.offset())); + } + } + }); +} +``` + +异步提交存在的问题是,在提交失败的时候不会进行自动重试,实际上也不能进行自动重试。假设程序同时提交了 200 和 300 的偏移量,此时 200 的偏移量失败的,但是紧随其后的 300 的偏移量成功了,此时如果重试就会存在 200 覆盖 300 偏移量的可能。同步提交就不存在这个问题,因为在同步提交的情况下,300 的提交请求必须等待服务器返回 200 提交请求的成功反馈后才会发出。基于这个原因,某些情况下,需要同时组合同步和异步两种提交方式。 + +> 注:虽然程序不能在失败时候进行自动重试,但是我们是可以手动进行重试的,你可以通过一个 Map offsets 来维护你提交的每个分区的偏移量,然后当失败时候,你可以判断失败的偏移量是否小于你维护的同主题同分区的最后提交的偏移量,如果小于则代表你已经提交了更大的偏移量请求,此时不需要重试,否则就可以进行手动重试。 + +### 4.3 同步加异步提交 + +下面这种情况,在正常的轮询中使用异步提交来保证吞吐量,但是因为在最后即将要关闭消费者了,所以此时需要用同步提交来保证最大限度的提交成功。 + +```scala +try { + while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.println(record); + } + // 异步提交 + consumer.commitAsync(); + } +} catch (Exception e) { + e.printStackTrace(); +} finally { + try { + // 因为即将要关闭消费者,所以要用同步提交保证提交成功 + consumer.commitSync(); + } finally { + consumer.close(); + } +} +``` + +### 4.4 提交特定偏移量 + +在上面同步和异步提交的 API 中,实际上我们都没有对 commit 方法传递参数,此时默认提交的是当前轮询的最大偏移量,如果你需要提交特定的偏移量,可以调用它们的重载方法。 + +```java +/*同步提交特定偏移量*/ +commitSync(Map offsets) +/*异步提交特定偏移量*/ +commitAsync(Map offsets, OffsetCommitCallback callback) +``` + +需要注意的是,因为你可以订阅多个主题,所以 `offsets` 中必须要包含所有主题的每个分区的偏移量,示例代码如下: + +```java +try { + while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.println(record); + /*记录每个主题的每个分区的偏移量*/ + TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); + OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset()+1, "no metaData"); + /*TopicPartition 重写过 hashCode 和 equals 方法,所以能够保证同一主题和分区的实例不会被重复添加*/ + offsets.put(topicPartition, offsetAndMetadata); + } + /*提交特定偏移量*/ + consumer.commitAsync(offsets, null); + } +} finally { + consumer.close(); +} +``` + + + +## 五、监听分区再均衡 + +因为分区再均衡会导致分区与消费者的重新划分,有时候你可能希望在再均衡前执行一些操作:比如提交已经处理但是尚未提交的偏移量,关闭数据库连接等。此时可以在订阅主题时候,调用 `subscribe` 的重载方法传入自定义的分区再均衡监听器。 + +```java + /*订阅指定集合内的所有主题*/ +subscribe(Collection topics, ConsumerRebalanceListener listener) + /*使用正则匹配需要订阅的主题*/ +subscribe(Pattern pattern, ConsumerRebalanceListener listener) +``` + +代码示例如下: + +```java +Map offsets = new HashMap<>(); + +consumer.subscribe(Collections.singletonList(topic), new ConsumerRebalanceListener() { + /*该方法会在消费者停止读取消息之后,再均衡开始之前就调用*/ + @Override + public void onPartitionsRevoked(Collection partitions) { + System.out.println("再均衡即将触发"); + // 提交已经处理的偏移量 + consumer.commitSync(offsets); + } + + /*该方法会在重新分配分区之后,消费者开始读取消息之前被调用*/ + @Override + public void onPartitionsAssigned(Collection partitions) { + + } +}); + +try { + while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.println(record); + TopicPartition topicPartition = new TopicPartition(record.topic(), record.partition()); + OffsetAndMetadata offsetAndMetadata = new OffsetAndMetadata(record.offset() + 1, "no metaData"); + /*TopicPartition 重写过 hashCode 和 equals 方法,所以能够保证同一主题和分区的实例不会被重复添加*/ + offsets.put(topicPartition, offsetAndMetadata); + } + consumer.commitAsync(offsets, null); + } +} finally { + consumer.close(); +} +``` + + + +## 六 、退出轮询 + +Kafka 提供了 `consumer.wakeup()` 方法用于退出轮询,它通过抛出 `WakeupException` 异常来跳出循环。需要注意的是,在退出线程时最好显示的调用 `consumer.close()` , 此时消费者会提交任何还没有提交的东西,并向群组协调器发送消息,告知自己要离开群组,接下来就会触发再均衡 ,而不需要等待会话超时。 + +下面的示例代码为监听控制台输出,当输入 `exit` 时结束轮询,关闭消费者并退出程序: + +```java +/*调用 wakeup 优雅的退出*/ +final Thread mainThread = Thread.currentThread(); +new Thread(() -> { + Scanner sc = new Scanner(System.in); + while (sc.hasNext()) { + if ("exit".equals(sc.next())) { + consumer.wakeup(); + try { + /*等待主线程完成提交偏移量、关闭消费者等操作*/ + mainThread.join(); + break; + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +}).start(); + +try { + while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord rd : records) { + System.out.printf("topic = %s,partition = %d, key = %s, value = %s, offset = %d,\n", + rd.topic(), rd.partition(), rd.key(), rd.value(), rd.offset()); + } + } +} catch (WakeupException e) { + //对于 wakeup() 调用引起的 WakeupException 异常可以不必处理 +} finally { + consumer.close(); + System.out.println("consumer 关闭"); +} +``` + + + +## 七、独立的消费者 + +因为 Kafka 的设计目标是高吞吐和低延迟,所以在 Kafka 中,消费者通常都是从属于某个群组的,这是因为单个消费者的处理能力是有限的。但是某些时候你的需求可能很简单,比如可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据,这个时候就不需要消费者群组和再均衡了, 只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量即可。 + +在这种情况下,就不需要订阅主题, 取而代之的是消费者为自己分配分区。 一个消费者可以订阅主题(井加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。 分配分区的示例代码如下: + +```java +List partitions = new ArrayList<>(); +List partitionInfos = consumer.partitionsFor(topic); + +/*可以指定读取哪些分区 如这里假设只读取主题的 0 分区*/ +for (PartitionInfo partition : partitionInfos) { + if (partition.partition()==0){ + partitions.add(new TopicPartition(partition.topic(), partition.partition())); + } +} + +// 为消费者指定分区 +consumer.assign(partitions); + + +while (true) { + ConsumerRecords records = consumer.poll(Duration.of(100, ChronoUnit.MILLIS)); + for (ConsumerRecord record : records) { + System.out.printf("partition = %s, key = %d, value = %s\n", + record.partition(), record.key(), record.value()); + } + consumer.commitSync(); +} +``` + + + +## 附录 : Kafka消费者可选属性 + +### 1. fetch.min.byte + +消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。 + +### 2. fetch.max.wait.ms + +broker 返回给消费者数据的等待时间,默认是 500ms。 + +### 3. max.partition.fetch.bytes + +该属性指定了服务器从每个分区返回给消费者的最大字节数,默认为 1MB。 + +### 4. session.timeout.ms + +消费者在被认为死亡之前可以与服务器断开连接的时间,默认是 3s。 + +### 5. auto.offset.reset + +该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: + +- latest (默认值) :在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的最新记录); +- earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。 + +### 6. enable.auto.commit + +是否自动提交偏移量,默认值是 true。为了避免出现重复消费和数据丢失,可以把它设置为 false。 + +### 7. client.id + +客户端 id,服务器用来识别消息的来源。 + +### 8. max.poll.records + +单次调用 `poll()` 方法能够返回的记录数量。 + +### 9. receive.buffer.bytes & send.buffer.byte + +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 + + + +## 参考资料 + +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\267\261\345\205\245\347\220\206\350\247\243\345\210\206\345\214\272\345\211\257\346\234\254\346\234\272\345\210\266.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\267\261\345\205\245\347\220\206\350\247\243\345\210\206\345\214\272\345\211\257\346\234\254\346\234\272\345\210\266.md" new file mode 100644 index 0000000..c6d5531 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\346\267\261\345\205\245\347\220\206\350\247\243\345\210\206\345\214\272\345\211\257\346\234\254\346\234\272\345\210\266.md" @@ -0,0 +1,161 @@ +# 深入理解Kafka副本机制 + + + + +## 一、Kafka集群 + +Kafka 使用 Zookeeper 来维护集群成员 (brokers) 的信息。每个 broker 都有一个唯一标识 `broker.id`,用于标识自己在集群中的身份,可以在配置文件 `server.properties` 中进行配置,或者由程序自动生成。下面是 Kafka brokers 集群自动创建的过程: + ++ 每一个 broker 启动的时候,它会在 Zookeeper 的 `/brokers/ids` 路径下创建一个 ` 临时节点 `,并将自己的 `broker.id` 写入,从而将自身注册到集群; ++ 当有多个 broker 时,所有 broker 会竞争性地在 Zookeeper 上创建 `/controller` 节点,由于 Zookeeper 上的节点不会重复,所以必然只会有一个 broker 创建成功,此时该 broker 称为 controller broker。它除了具备其他 broker 的功能外,**还负责管理主题分区及其副本的状态**。 ++ 当 broker 出现宕机或者主动退出从而导致其持有的 Zookeeper 会话超时时,会触发注册在 Zookeeper 上的 watcher 事件,此时 Kafka 会进行相应的容错处理;如果宕机的是 controller broker 时,还会触发新的 controller 选举。 + +## 二、副本机制 + +为了保证高可用,kafka 的分区是多副本的,如果一个副本丢失了,那么还可以从其他副本中获取分区数据。但是这要求对应副本的数据必须是完整的,这是 Kafka 数据一致性的基础,所以才需要使用 `controller broker` 来进行专门的管理。下面将详解介绍 Kafka 的副本机制。 + +### 2.1 分区和副本 + +Kafka 的主题被分为多个分区 ,分区是 Kafka 最基本的存储单位。每个分区可以有多个副本 (可以在创建主题时使用 ` replication-factor` 参数进行指定)。其中一个副本是首领副本 (Leader replica),所有的事件都直接发送给首领副本;其他副本是跟随者副本 (Follower replica),需要通过复制来保持与首领副本数据一致,当首领副本不可用时,其中一个跟随者副本将成为新首领。 + +
+ +### 2.2 ISR机制 + +每个分区都有一个 ISR(in-sync Replica) 列表,用于维护所有同步的、可用的副本。首领副本必然是同步副本,而对于跟随者副本来说,它需要满足以下条件才能被认为是同步副本: + ++ 与 Zookeeper 之间有一个活跃的会话,即必须定时向 Zookeeper 发送心跳; ++ 在规定的时间内从首领副本那里低延迟地获取过消息。 + +如果副本不满足上面条件的话,就会被从 ISR 列表中移除,直到满足条件才会被再次加入。 + +这里给出一个主题创建的示例:使用 `--replication-factor` 指定副本系数为 3,创建成功后使用 `--describe ` 命令可以看到分区 0 的有 0,1,2 三个副本,且三个副本都在 ISR 列表中,其中 1 为首领副本。 + +
+ +### 2.3 不完全的首领选举 + +对于副本机制,在 broker 级别有一个可选的配置参数 `unclean.leader.election.enable`,默认值为 fasle,代表禁止不完全的首领选举。这是针对当首领副本挂掉且 ISR 中没有其他可用副本时,是否允许某个不完全同步的副本成为首领副本,这可能会导致数据丢失或者数据不一致,在某些对数据一致性要求较高的场景 (如金融领域),这可能无法容忍的,所以其默认值为 false,如果你能够允许部分数据不一致的话,可以配置为 true。 + +### 2.4 最少同步副本 + +ISR 机制的另外一个相关参数是 `min.insync.replicas` , 可以在 broker 或者主题级别进行配置,代表 ISR 列表中至少要有几个可用副本。这里假设设置为 2,那么当可用副本数量小于该值时,就认为整个分区处于不可用状态。此时客户端再向分区写入数据时候就会抛出异常 `org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required。` + +### 2.5 发送确认 + +Kafka 在生产者上有一个可选的参数 ack,该参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入成功: + +- **acks=0** :消息发送出去就认为已经成功了,不会等待任何来自服务器的响应; +- **acks=1** : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应; +- **acks=all** :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。 + +## 三、数据请求 + +### 3.1 元数据请求机制 + +在所有副本中,只有领导副本才能进行消息的读写处理。由于不同分区的领导副本可能在不同的 broker 上,如果某个 broker 收到了一个分区请求,但是该分区的领导副本并不在该 broker 上,那么它就会向客户端返回一个 `Not a Leader for Partition` 的错误响应。 为了解决这个问题,Kafka 提供了元数据请求机制。 + +首先集群中的每个 broker 都会缓存所有主题的分区副本信息,客户端会定期发送发送元数据请求,然后将获取的元数据进行缓存。定时刷新元数据的时间间隔可以通过为客户端配置 `metadata.max.age.ms` 来进行指定。有了元数据信息后,客户端就知道了领导副本所在的 broker,之后直接将读写请求发送给对应的 broker 即可。 + +如果在定时请求的时间间隔内发生的分区副本的选举,则意味着原来缓存的信息可能已经过时了,此时还有可能会收到 `Not a Leader for Partition` 的错误响应,这种情况下客户端会再次求发出元数据请求,然后刷新本地缓存,之后再去正确的 broker 上执行对应的操作,过程如下图: + +
+ +### 3.2 数据可见性 + +需要注意的是,并不是所有保存在分区首领上的数据都可以被客户端读取到,为了保证数据一致性,只有被所有同步副本 (ISR 中所有副本) 都保存了的数据才能被客户端读取到。 + +
+ +### 3.3 零拷贝 + +Kafka 所有数据的写入和读取都是通过零拷贝来实现的。传统拷贝与零拷贝的区别如下: + +#### 传统模式下的四次拷贝与四次上下文切换 + +以将磁盘文件通过网络发送为例。传统模式下,一般使用如下伪代码所示的方法先将文件数据读入内存,然后通过 Socket 将内存中的数据发送出去。 + +```java +buffer = File.read +Socket.send(buffer) +``` + +这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝),然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还伴随着四次上下文切换,如下图所示: + +
+ +#### sendfile和transferTo实现零拷贝 + +Linux 2.4+ 内核通过 `sendfile` 系统调用,提供了零拷贝。数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer,无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件到网络发送由一个 `sendfile` 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示: + +
+ +从具体实现来看,Kafka 的数据传输通过 TransportLayer 来完成,其子类 `PlaintextTransportLayer` 的 `transferFrom` 方法通过调用 Java NIO 中 FileChannel 的 `transferTo` 方法实现零拷贝,如下所示: + +```java +@Override +public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException { + return fileChannel.transferTo(position, count, socketChannel); +} +``` + +**注:** `transferTo` 和 `transferFrom` 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 `sendfile` 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。 + +## 四、物理存储 + +### 4.1 分区分配 + +在创建主题时,Kafka 会首先决定如何在 broker 间分配分区副本,它遵循以下原则: + ++ 在所有 broker 上均匀地分配分区副本; ++ 确保分区的每个副本分布在不同的 broker 上; ++ 如果使用了 `broker.rack` 参数为 broker 指定了机架信息,那么会尽可能的把每个分区的副本分配到不同机架的 broker 上,以避免一个机架不可用而导致整个分区不可用。 + +基于以上原因,如果你在一个单节点上创建一个 3 副本的主题,通常会抛出下面的异常: + +```properties +Error while executing topic command : org.apache.kafka.common.errors.InvalidReplicationFactor +Exception: Replication factor: 3 larger than available brokers: 1. +``` + +### 4.2 分区数据保留规则 + +保留数据是 Kafka 的一个基本特性, 但是 Kafka 不会一直保留数据,也不会等到所有消费者都读取了消息之后才删除消息。相反, Kafka 为每个主题配置了数据保留期限,规定数据被删除之前可以保留多长时间,或者清理数据之前可以保留的数据量大小。分别对应以下四个参数: + +- `log.retention.bytes` :删除数据前允许的最大数据量;默认值-1,代表没有限制; +- `log.retention.ms`:保存数据文件的毫秒数,如果未设置,则使用 `log.retention.minutes` 中的值,默认为 null; +- `log.retention.minutes`:保留数据文件的分钟数,如果未设置,则使用 `log.retention.hours` 中的值,默认为 null; +- `log.retention.hours`:保留数据文件的小时数,默认值为 168,也就是一周。 + +因为在一个大文件里查找和删除消息是很费时的,也很容易出错,所以 Kafka 把分区分成若干个片段,当前正在写入数据的片段叫作活跃片段。活动片段永远不会被删除。如果按照默认值保留数据一周,而且每天使用一个新片段,那么你就会看到,在每天使用一个新片段的同时会删除一个最老的片段,所以大部分时间该分区会有 7 个片段存在。 + +### 4.3 文件格式 + +通常保存在磁盘上的数据格式与生产者发送过来消息格式是一样的。 如果生产者发送的是压缩过的消息,那么同一个批次的消息会被压缩在一起,被当作“包装消息”进行发送 (格式如下所示) ,然后保存到磁盘上。之后消费者读取后再自己解压这个包装消息,获取每条消息的具体信息。 + +
+ + + +## 参考资料 + +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 +2. [Kafka 高性能架构之道](http://www.jasongj.com/kafka/high_throughput/) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\224\237\344\272\247\350\200\205\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\224\237\344\272\247\350\200\205\350\257\246\350\247\243.md" new file mode 100644 index 0000000..e9f68d8 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\224\237\344\272\247\350\200\205\350\257\246\350\247\243.md" @@ -0,0 +1,364 @@ +# Kafka生产者详解 + + + + +## 一、生产者发送消息的过程 + +首先介绍一下 Kafka 生产者发送消息的过程: + ++ Kafka 会将发送消息包装为 ProducerRecord 对象, ProducerRecord 对象包含了目标主题和要发送的内容,同时还可以指定键和分区。在发送 ProducerRecord 对象前,生产者会先把键和值对象序列化成字节数组,这样它们才能够在网络上传输。 ++ 接下来,数据被传给分区器。如果之前已经在 ProducerRecord 对象里指定了分区,那么分区器就不会再做任何事情。如果没有指定分区 ,那么分区器会根据 ProducerRecord 对象的键来选择一个分区,紧接着,这条记录被添加到一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。有一个独立的线程负责把这些记录批次发送到相应的 broker 上。 ++ 服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka,就返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入失败,则会返回一个错误。生产者在收到错误之后会尝试重新发送消息,如果达到指定的重试次数后还没有成功,则直接抛出异常,不再重试。 + +
+ +## 二、创建生产者 + +### 2.1 项目依赖 + +本项目采用 Maven 构建,想要调用 Kafka 生产者 API,需要导入 `kafka-clients` 依赖,如下: + +```xml + + org.apache.kafka + kafka-clients + 2.2.0 + +``` + +### 2.2 创建生产者 + +创建 Kafka 生产者时,以下三个属性是必须指定的: + ++ **bootstrap.servers** :指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找 broker 的信息。不过建议至少要提供两个 broker 的信息作为容错; ++ **key.serializer** :指定键的序列化器; ++ **value.serializer** :指定值的序列化器。 + +创建的示例代码如下: + +```scala +public class SimpleProducer { + + public static void main(String[] args) { + + String topicName = "Hello-Kafka"; + + Properties props = new Properties(); + props.put("bootstrap.servers", "hadoop001:9092"); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + /*创建生产者*/ + Producer producer = new KafkaProducer<>(props); + + for (int i = 0; i < 10; i++) { + ProducerRecord record = new ProducerRecord<>(topicName, "hello" + i, + "world" + i); + /* 发送消息*/ + producer.send(record); + } + /*关闭生产者*/ + producer.close(); + } +} +``` + +> 本篇文章的所有示例代码可以从 Github 上进行下载:[kafka-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/Kafka/kafka-basis) + +### 2.3 测试 + +#### 1. 启动Kakfa + +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: + +```shell +# zookeeper启动命令 +bin/zkServer.sh start + +# 内置zookeeper启动命令 +bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动单节点 kafka 用于测试: + +```shell +# bin/kafka-server-start.sh config/server.properties +``` + +#### 2. 创建topic + +```shell +# 创建用于测试主题 +bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 --partitions 1 \ + --topic Hello-Kafka + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +#### 3. 启动消费者 + + 启动一个控制台消费者用于观察写入情况,启动命令如下: + +```shell +# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic Hello-Kafka --from-beginning +``` + +#### 4. 运行项目 + +此时可以看到消费者控制台,输出如下,这里 `kafka-console-consumer` 只会打印出值信息,不会打印出键信息。 + +
+ + + +### 2.4 可能出现的问题 + +在这里可能出现的一个问题是:生产者程序在启动后,一直处于等待状态。这通常出现在你使用默认配置启动 Kafka 的情况下,此时需要对 `server.properties` 文件中的 `listeners` 配置进行更改: + +```shell +# hadoop001 为我启动kafka服务的主机名,你可以换成自己的主机名或者ip地址 +listeners=PLAINTEXT://hadoop001:9092 +``` + + + +## 二、发送消息 + +上面的示例程序调用了 `send` 方法发送消息后没有做任何操作,在这种情况下,我们没有办法知道消息发送的结果。想要知道消息发送的结果,可以使用同步发送或者异步发送来实现。 + +### 2.1 同步发送 + +在调用 `send` 方法后可以接着调用 `get()` 方法,`send` 方法的返回值是一个 Future\对象,RecordMetadata 里面包含了发送消息的主题、分区、偏移量等信息。改写后的代码如下: + +```scala +for (int i = 0; i < 10; i++) { + try { + ProducerRecord record = new ProducerRecord<>(topicName, "k" + i, "world" + i); + /*同步发送消息*/ + RecordMetadata metadata = producer.send(record).get(); + System.out.printf("topic=%s, partition=%d, offset=%s \n", + metadata.topic(), metadata.partition(), metadata.offset()); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } +} +``` + +此时得到的输出如下:偏移量和调用次数有关,所有记录都分配到了 0 分区,这是因为在创建 `Hello-Kafka` 主题时候,使用 `--partitions` 指定其分区数为 1,即只有一个分区。 + +```shell +topic=Hello-Kafka, partition=0, offset=40 +topic=Hello-Kafka, partition=0, offset=41 +topic=Hello-Kafka, partition=0, offset=42 +topic=Hello-Kafka, partition=0, offset=43 +topic=Hello-Kafka, partition=0, offset=44 +topic=Hello-Kafka, partition=0, offset=45 +topic=Hello-Kafka, partition=0, offset=46 +topic=Hello-Kafka, partition=0, offset=47 +topic=Hello-Kafka, partition=0, offset=48 +topic=Hello-Kafka, partition=0, offset=49 +``` + +### 2.2 异步发送 + +通常我们并不关心发送成功的情况,更多关注的是失败的情况,因此 Kafka 提供了异步发送和回调函数。 代码如下: + +```scala +for (int i = 0; i < 10; i++) { + ProducerRecord record = new ProducerRecord<>(topicName, "k" + i, "world" + i); + /*异步发送消息,并监听回调*/ + producer.send(record, new Callback() { + @Override + public void onCompletion(RecordMetadata metadata, Exception exception) { + if (exception != null) { + System.out.println("进行异常处理"); + } else { + System.out.printf("topic=%s, partition=%d, offset=%s \n", + metadata.topic(), metadata.partition(), metadata.offset()); + } + } + }); +} +``` + + + +## 三、自定义分区器 + +Kafka 有着默认的分区机制: + ++ 如果键值为 null, 则使用轮询 (Round Robin) 算法将消息均衡地分布到各个分区上; ++ 如果键值不为 null,那么 Kafka 会使用内置的散列算法对键进行散列,然后分布到各个分区上。 + +某些情况下,你可能有着自己的分区需求,这时候可以采用自定义分区器实现。这里给出一个自定义分区器的示例: + +### 3.1 自定义分区器 + +```java +/** + * 自定义分区器 + */ +public class CustomPartitioner implements Partitioner { + + private int passLine; + + @Override + public void configure(Map configs) { + /*从生产者配置中获取分数线*/ + passLine = (Integer) configs.get("pass.line"); + } + + @Override + public int partition(String topic, Object key, byte[] keyBytes, Object value, + byte[] valueBytes, Cluster cluster) { + /*key 值为分数,当分数大于分数线时候,分配到 1 分区,否则分配到 0 分区*/ + return (Integer) key >= passLine ? 1 : 0; + } + + @Override + public void close() { + System.out.println("分区器关闭"); + } +} +``` + +需要在创建生产者时指定分区器,和分区器所需要的配置参数: + +```java +public class ProducerWithPartitioner { + + public static void main(String[] args) { + + String topicName = "Kafka-Partitioner-Test"; + + Properties props = new Properties(); + props.put("bootstrap.servers", "hadoop001:9092"); + props.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + + /*传递自定义分区器*/ + props.put("partitioner.class", "com.heibaiying.producers.partitioners.CustomPartitioner"); + /*传递分区器所需的参数*/ + props.put("pass.line", 6); + + Producer producer = new KafkaProducer<>(props); + + for (int i = 0; i <= 10; i++) { + String score = "score:" + i; + ProducerRecord record = new ProducerRecord<>(topicName, i, score); + /*异步发送消息*/ + producer.send(record, (metadata, exception) -> + System.out.printf("%s, partition=%d, \n", score, metadata.partition())); + } + + producer.close(); + } +} +``` + +### 3.2 测试 + +需要创建一个至少有两个分区的主题: + +```shell + bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 --partitions 2 \ + --topic Kafka-Partitioner-Test +``` + +此时输入如下,可以看到分数大于等于 6 分的都被分到 1 分区,而小于 6 分的都被分到了 0 分区。 + +```shell +score:6, partition=1, +score:7, partition=1, +score:8, partition=1, +score:9, partition=1, +score:10, partition=1, +score:0, partition=0, +score:1, partition=0, +score:2, partition=0, +score:3, partition=0, +score:4, partition=0, +score:5, partition=0, +分区器关闭 +``` + + + +## 四、生产者其他属性 + +上面生产者的创建都仅指定了服务地址,键序列化器、值序列化器,实际上 Kafka 的生产者还有很多可配置属性,如下: + +### 1. acks + +acks 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的: + +- **acks=0** : 消息发送出去就认为已经成功了,不会等待任何来自服务器的响应; +- **acks=1** : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应; +- **acks=all** :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。 + +### 2. buffer.memory + +设置生产者内存缓冲区的大小。 + +### 3. compression.type + +默认情况下,发送的消息不会被压缩。如果想要进行压缩,可以配置此参数,可选值有 snappy,gzip,lz4。 + +### 4. retries + +发生错误后,消息重发的次数。如果达到设定值,生产者就会放弃重试并返回错误。 + +### 5. batch.size + +当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。 + +### 6. linger.ms + +该参数制定了生产者在发送批次之前等待更多消息加入批次的时间。 + +### 7. clent.id + +客户端 id,服务器用来识别消息的来源。 + +### 8. max.in.flight.requests.per.connection + +指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量,把它设置为 1 可以保证消息是按照发送的顺序写入服务器,即使发生了重试。 + +### 9. timeout.ms, request.timeout.ms & metadata.fetch.timeout.ms + +- timeout.ms 指定了 borker 等待同步副本返回消息的确认时间; +- request.timeout.ms 指定了生产者在发送数据时等待服务器返回响应的时间; +- metadata.fetch.timeout.ms 指定了生产者在获取元数据(比如分区首领是谁)时等待服务器返回响应的时间。 + +### 10. max.block.ms + +指定了在调用 `send()` 方法或使用 `partitionsFor()` 方法获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法会阻塞。在阻塞时间达到 max.block.ms 时,生产者会抛出超时异常。 + +### 11. max.request.size + +该参数用于控制生产者发送的请求大小。它可以指发送的单个消息的最大值,也可以指单个请求里所有消息总的大小。例如,假设这个值为 1000K ,那么可以发送的单个最大消息为 1000K ,或者生产者可以在单个请求里发送一个批次,该批次包含了 1000 个消息,每个消息大小为 1K。 + +### 12. receive.buffer.bytes & send.buffer.byte + +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 + + + + + +## 参考资料 + +1. Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\256\200\344\273\213.md" new file mode 100644 index 0000000..5ef5ef3 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Kafka\347\256\200\344\273\213.md" @@ -0,0 +1,67 @@ +# Kafka简介 + + + + +## 一、简介 + +ApacheKafka 是一个分布式的流处理平台。它具有以下特点: + ++ 支持消息的发布和订阅,类似于 RabbtMQ、ActiveMQ 等消息队列; ++ 支持数据实时处理; ++ 能保证消息的可靠性投递; ++ 支持消息的持久化存储,并通过多副本分布式的存储方案来保证消息的容错; ++ 高吞吐率,单 Broker 可以轻松处理数千个分区以及每秒百万级的消息量。 + +## 二、基本概念 + +### 2.1 Messages And Batches + +Kafka 的基本数据单元被称为 message(消息),为减少网络开销,提高效率,多个消息会被放入同一批次 (Batch) 中后再写入。 + +### 2.2 Topics And Partitions + +Kafka 的消息通过 Topics(主题) 进行分类,一个主题可以被分为若干个 Partitions(分区),一个分区就是一个提交日志 (commit log)。消息以追加的方式写入分区,然后以先入先出的顺序读取。Kafka 通过分区来实现数据的冗余和伸缩性,分区可以分布在不同的服务器上,这意味着一个 Topic 可以横跨多个服务器,以提供比单个服务器更强大的性能。 + +由于一个 Topic 包含多个分区,因此无法在整个 Topic 范围内保证消息的顺序性,但可以保证消息在单个分区内的顺序性。 + +
+ +### 2.3 Producers And Consumers + +#### 1. 生产者 + +生产者负责创建消息。一般情况下,生产者在把消息均衡地分布到在主题的所有分区上,而并不关心消息会被写到哪个分区。如果我们想要把消息写到指定的分区,可以通过自定义分区器来实现。 + +#### 2. 消费者 + +消费者是消费者群组的一部分,消费者负责消费消息。消费者可以订阅一个或者多个主题,并按照消息生成的顺序来读取它们。消费者通过检查消息的偏移量 (offset) 来区分读取过的消息。偏移量是一个不断递增的数值,在创建消息时,Kafka 会把它添加到其中,在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的偏移量保存在 Zookeeper 或 Kafka 上,如果消费者关闭或者重启,它还可以重新获取该偏移量,以保证读取状态不会丢失。 + +
+ +一个分区只能被同一个消费者群组里面的一个消费者读取,但可以被不同消费者群组中所组成的多个消费者共同读取。多个消费者群组中消费者共同读取同一个主题时,彼此之间互不影响。 + +
+ +### 2.4 Brokers And Clusters + +一个独立的 Kafka 服务器被称为 Broker。Broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。Broker 为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘的消息。 + +Broker 是集群 (Cluster) 的组成部分。每一个集群都会选举出一个 Broker 作为集群控制器 (Controller),集群控制器负责管理工作,包括将分区分配给 Broker 和监控 Broker。 + +在集群中,一个分区 (Partition) 从属一个 Broker,该 Broker 被称为分区的首领 (Leader)。一个分区可以分配给多个 Brokers,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个 Broker 失效,其他 Broker 可以接管领导权。 + +
+ + + +## 参考资料 + +Neha Narkhede, Gwen Shapira ,Todd Palino(著) , 薛命灯 (译) . Kafka 权威指南 . 人民邮电出版社 . 2017-12-26 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\207\275\346\225\260\345\222\214\351\227\255\345\214\205.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\207\275\346\225\260\345\222\214\351\227\255\345\214\205.md" new file mode 100644 index 0000000..482255c --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\207\275\346\225\260\345\222\214\351\227\255\345\214\205.md" @@ -0,0 +1,312 @@ +# 函数和闭包 + + + + +## 一、函数 + +### 1.1 函数与方法 + +Scala 中函数与方法的区别非常小,如果函数作为某个对象的成员,这样的函数被称为方法,否则就是一个正常的函数。 + +```scala +// 定义方法 +def multi1(x:Int) = {x * x} +// 定义函数 +val multi2 = (x: Int) => {x * x} + +println(multi1(3)) //输出 9 +println(multi2(3)) //输出 9 +``` + +也可以使用 `def` 定义函数: + +```scala +def multi3 = (x: Int) => {x * x} +println(multi3(3)) //输出 9 +``` + +`multi2` 和 `multi3` 本质上没有区别,这是因为函数是一等公民,`val multi2 = (x: Int) => {x * x}` 这个语句相当于是使用 `def` 预先定义了函数,之后赋值给变量 `multi2`。 + +### 1.2 函数类型 + +上面我们说过 `multi2` 和 `multi3` 本质上是一样的,那么作为函数它们是什么类型的?两者的类型实际上都是 `Int => Int`,前面一个 Int 代表输入参数类型,后面一个 Int 代表返回值类型。 + +```scala +scala> val multi2 = (x: Int) => {x * x} +multi2: Int => Int = $$Lambda$1092/594363215@1dd1a777 + +scala> def multi3 = (x: Int) => {x * x} +multi3: Int => Int + +// 如果有多个参数,则类型为:(参数类型,参数类型 ...)=>返回值类型 +scala> val multi4 = (x: Int,name: String) => {name + x * x } +multi4: (Int, String) => String = $$Lambda$1093/1039732747@2eb4fe7 +``` + +### 1.3 一等公民&匿名函数 + +在 Scala 中函数是一等公民,这意味着不仅可以定义函数并调用它们,还可以将它们作为值进行传递: + +```scala +import scala.math.ceil +object ScalaApp extends App { + // 将函数 ceil 赋值给变量 fun,使用下划线 (_) 指明是 ceil 函数但不传递参数 + val fun = ceil _ + println(fun(2.3456)) //输出 3.0 + +} +``` + +在 Scala 中你不必给每一个函数都命名,如 `(x: Int) => 3 * x` 就是一个匿名函数: + +```scala +object ScalaApp extends App { + // 1.匿名函数 + (x: Int) => 3 * x + // 2.具名函数 + val fun = (x: Int) => 3 * x + // 3.直接使用匿名函数 + val array01 = Array(1, 2, 3).map((x: Int) => 3 * x) + // 4.使用占位符简写匿名函数 + val array02 = Array(1, 2, 3).map(_ * 3) + // 5.使用具名函数 + val array03 = Array(1, 2, 3).map(fun) + +} +``` + +### 1.4 特殊的函数表达式 + +#### 1. 可变长度参数列表 + +在 Java 中如果你想要传递可变长度的参数,需要使用 `String ...args` 这种形式,Scala 中等效的表达为 `args: String*`。 + +```scala +object ScalaApp extends App { + def echo(args: String*): Unit = { + for (arg <- args) println(arg) + } + echo("spark","hadoop","flink") +} +// 输出 +spark +hadoop +flink +``` + +#### 2. 传递具名参数 + +向函数传递参数时候可以指定具体的参数名。 + +```scala +object ScalaApp extends App { + + def detail(name: String, age: Int): Unit = println(name + ":" + age) + + // 1.按照参数定义的顺序传入 + detail("heibaiying", 12) + // 2.传递参数的时候指定具体的名称,则不必遵循定义的顺序 + detail(age = 12, name = "heibaiying") + +} +``` + +#### 3. 默认值参数 + +在定义函数时,可以为参数指定默认值。 + +```scala +object ScalaApp extends App { + + def detail(name: String, age: Int = 88): Unit = println(name + ":" + age) + + // 如果没有传递 age 值,则使用默认值 + detail("heibaiying") + detail("heibaiying", 12) + +} +``` + +## 二、闭包 + +### 2.1 闭包的定义 + +```scala +var more = 10 +// addMore 一个闭包函数:因为其捕获了自由变量 more 从而闭合了该函数字面量 +val addMore = (x: Int) => x + more +``` + +如上函数 `addMore` 中有两个变量 x 和 more: + ++ **x** : 是一个绑定变量 (bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; ++ **more** : 是一个自由变量 (free variable),因为函数字面量本生并没有给 more 赋予任何含义。 + +按照定义:在创建函数时,如果需要捕获自由变量,那么包含指向被捕获变量的引用的函数就被称为闭包函数。 + +### 2.2 修改自由变量 + +这里需要注意的是,闭包捕获的是变量本身,即是对变量本身的引用,这意味着: + ++ 闭包外部对自由变量的修改,在闭包内部是可见的; ++ 闭包内部对自由变量的修改,在闭包外部也是可见的。 + +```scala +// 声明 more 变量 +scala> var more = 10 +more: Int = 10 + +// more 变量必须已经被声明,否则下面的语句会报错 +scala> val addMore = (x: Int) => {x + more} +addMore: Int => Int = $$Lambda$1076/1844473121@876c4f0 + +scala> addMore(10) +res7: Int = 20 + +// 注意这里是给 more 变量赋值,而不是重新声明 more 变量 +scala> more=1000 +more: Int = 1000 + +scala> addMore(10) +res8: Int = 1010 +``` + +### 2.3 自由变量多副本 + +自由变量可能随着程序的改变而改变,从而产生多个副本,但是闭包永远指向创建时候有效的那个变量副本。 + +```scala +// 第一次声明 more 变量 +scala> var more = 10 +more: Int = 10 + +// 创建闭包函数 +scala> val addMore10 = (x: Int) => {x + more} +addMore10: Int => Int = $$Lambda$1077/1144251618@1bdaa13c + +// 调用闭包函数 +scala> addMore10(9) +res9: Int = 19 + +// 重新声明 more 变量 +scala> var more = 100 +more: Int = 100 + +// 创建新的闭包函数 +scala> val addMore100 = (x: Int) => {x + more} +addMore100: Int => Int = $$Lambda$1078/626955849@4d0be2ac + +// 引用的是重新声明 more 变量 +scala> addMore100(9) +res10: Int = 109 + +// 引用的还是第一次声明的 more 变量 +scala> addMore10(9) +res11: Int = 19 + +// 对于全局而言 more 还是 100 +scala> more +res12: Int = 100 +``` + +从上面的示例可以看出重新声明 `more` 后,全局的 `more` 的值是 100,但是对于闭包函数 `addMore10` 还是引用的是值为 10 的 `more`,这是由虚拟机来实现的,虚拟机会保证 `more` 变量在重新声明后,原来的被捕获的变量副本继续在堆上保持存活。 + +## 三、高阶函数 + +### 3.1 使用函数作为参数 + +定义函数时候支持传入函数作为参数,此时新定义的函数被称为高阶函数。 + +```scala +object ScalaApp extends App { + + // 1.定义函数 + def square = (x: Int) => { + x * x + } + + // 2.定义高阶函数: 第一个参数是类型为 Int => Int 的函数 + def multi(fun: Int => Int, x: Int) = { + fun(x) * 100 + } + + // 3.传入具名函数 + println(multi(square, 5)) // 输出 2500 + + // 4.传入匿名函数 + println(multi(_ * 100, 5)) // 输出 50000 + +} +``` + +### 3.2 函数柯里化 + +我们上面定义的函数都只支持一个参数列表,而柯里化函数则支持多个参数列表。柯里化指的是将原来接受两个参数的函数变成接受一个参数的函数的过程。新的函数以原有第二个参数作为参数。 + +```scala +object ScalaApp extends App { + // 定义柯里化函数 + def curriedSum(x: Int)(y: Int) = x + y + println(curriedSum(2)(3)) //输出 5 +} +``` + +这里当你调用 curriedSum 时候,实际上是连着做了两次传统的函数调用,实际执行的柯里化过程如下: + ++ 第一次调用接收一个名为 `x` 的 Int 型参数,返回一个用于第二次调用的函数,假设 `x` 为 2,则返回函数 `2+y`; ++ 返回的函数接收参数 `y`,并计算并返回值 `2+3` 的值。 + +想要获得柯里化的中间返回的函数其实也比较简单: + +```scala +object ScalaApp extends App { + // 定义柯里化函数 + def curriedSum(x: Int)(y: Int) = x + y + println(curriedSum(2)(3)) //输出 5 + + // 获取传入值为 10 返回的中间函数 10 + y + val plus: Int => Int = curriedSum(10)_ + println(plus(3)) //输出值 13 +} +``` + +柯里化支持多个参数列表,多个参数按照从左到右的顺序依次执行柯里化操作: + +```scala +object ScalaApp extends App { + // 定义柯里化函数 + def curriedSum(x: Int)(y: Int)(z: String) = x + y + z + println(curriedSum(2)(3)("name")) // 输出 5name + +} +``` + + + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + + + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\210\227\350\241\250\345\222\214\351\233\206.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\210\227\350\241\250\345\222\214\351\233\206.md" new file mode 100644 index 0000000..208d42e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\210\227\350\241\250\345\222\214\351\233\206.md" @@ -0,0 +1,542 @@ +# List & Set + + + + + +## 一、List字面量 + +List 是 Scala 中非常重要的一个数据结构,其与 Array(数组) 非常类似,但是 List 是不可变的,和 Java 中的 List 一样,其底层实现是链表。 + +```scala +scala> val list = List("hadoop", "spark", "storm") +list: List[String] = List(hadoop, spark, storm) + +// List 是不可变 +scala> list(1) = "hive" +:9: error: value update is not a member of List[String] +``` + +## 二、List类型 + +Scala 中 List 具有以下两个特性: + ++ **同构 (homogeneous)**:同一个 List 中的所有元素都必须是相同的类型; ++ **协变 (covariant)**:如果 S 是 T 的子类型,那么 `List[S]` 就是 `List[T]` 的子类型,例如 `List[String]` 是 `List[Object]` 的子类型。 + +需要特别说明的是空列表的类型为 `List[Nothing]`: + +```scala +scala> List() +res1: List[Nothing] = List() +``` + +## 三、构建List + +所有 List 都由两个基本单元构成:`Nil` 和 `::`(读作"cons")。即列表要么是空列表 (Nil),要么是由一个 head 加上一个 tail 组成,而 tail 又是一个 List。我们在上面使用的 `List("hadoop", "spark", "storm")` 最终也是被解释为 ` "hadoop"::"spark":: "storm"::Nil`。 + +```scala +scala> val list01 = "hadoop"::"spark":: "storm"::Nil +list01: List[String] = List(hadoop, spark, storm) + +// :: 操作符号是右结合的,所以上面的表达式和下面的等同 +scala> val list02 = "hadoop"::("spark":: ("storm"::Nil)) +list02: List[String] = List(hadoop, spark, storm) +``` + +## 四、模式匹配 + +Scala 支持展开列表以实现模式匹配。 + +```scala +scala> val list = List("hadoop", "spark", "storm") +list: List[String] = List(hadoop, spark, storm) + +scala> val List(a,b,c)=list +a: String = hadoop +b: String = spark +c: String = storm +``` + +如果只需要匹配部分内容,可以如下: + +```scala +scala> val a::rest=list +a: String = hadoop +rest: List[String] = List(spark, storm) +``` + +## 五、列表的基本操作 + +### 5.1 常用方法 + +```scala +object ScalaApp extends App { + + val list = List("hadoop", "spark", "storm") + + // 1.列表是否为空 + list.isEmpty + + // 2.返回列表中的第一个元素 + list.head + + // 3.返回列表中除第一个元素外的所有元素 这里输出 List(spark, storm) + list.tail + + // 4.tail 和 head 可以结合使用 + list.tail.head + + // 5.返回列表中的最后一个元素 与 head 相反 + list.init + + // 6.返回列表中除了最后一个元素之外的其他元素 与 tail 相反 这里输出 List(hadoop, spark) + list.last + + // 7.使用下标访问元素 + list(2) + + // 8.获取列表长度 + list.length + + // 9. 反转列表 + list.reverse + +} +``` + +### 5.2 indices + +indices 方法返回所有下标。 + +```scala +scala> list.indices +res2: scala.collection.immutable.Range = Range(0, 1, 2) +``` + +### 5.3 take & drop & splitAt + +- **take**:获取前 n 个元素; +- **drop**:删除前 n 个元素; +- **splitAt**:从第几个位置开始拆分。 + +```scala +scala> list take 2 +res3: List[String] = List(hadoop, spark) + +scala> list drop 2 +res4: List[String] = List(storm) + +scala> list splitAt 2 +res5: (List[String], List[String]) = (List(hadoop, spark),List(storm)) +``` + +### 5.4 flatten + +flatten 接收一个由列表组成的列表,并将其进行扁平化操作,返回单个列表。 + +```scala +scala> List(List(1, 2), List(3), List(), List(4, 5)).flatten +res6: List[Int] = List(1, 2, 3, 4, 5) +``` + +### 5.5 zip & unzip + +对两个 List 执行 `zip` 操作结果如下,返回对应位置元素组成的元组的列表,`unzip` 则执行反向操作。 + +```scala +scala> val list = List("hadoop", "spark", "storm") +scala> val score = List(10,20,30) + +scala> val zipped=list zip score +zipped: List[(String, Int)] = List((hadoop,10), (spark,20), (storm,30)) + +scala> zipped.unzip +res7: (List[String], List[Int]) = (List(hadoop, spark, storm),List(10, 20, 30)) +``` + +### 5.6 toString & mkString + +toString 返回 List 的字符串表现形式。 + +```scala +scala> list.toString +res8: String = List(hadoop, spark, storm) +``` + +如果想改变 List 的字符串表现形式,可以使用 mkString。mkString 有三个重载方法,方法定义如下: + +```scala +// start:前缀 sep:分隔符 end:后缀 +def mkString(start: String, sep: String, end: String): String = + addString(new StringBuilder(), start, sep, end).toString + +// seq 分隔符 +def mkString(sep: String): String = mkString("", sep, "") + +// 如果不指定分隔符 默认使用""分隔 +def mkString: String = mkString("") +``` + +使用示例如下: + +```scala +scala> list.mkString +res9: String = hadoopsparkstorm + +scala> list.mkString(",") +res10: String = hadoop,spark,storm + +scala> list.mkString("{",",","}") +res11: String = {hadoop,spark,storm} +``` + +### 5.7 iterator & toArray & copyToArray + +iterator 方法返回的是迭代器,这和其他语言的使用是一样的。 + +```scala +object ScalaApp extends App { + + val list = List("hadoop", "spark", "storm") + + val iterator: Iterator[String] = list.iterator + + while (iterator.hasNext) { + println(iterator.next) + } + +} +``` + +toArray 和 toList 用于 List 和数组之间的互相转换。 + +```scala +scala> val array = list.toArray +array: Array[String] = Array(hadoop, spark, storm) + +scala> array.toList +res13: List[String] = List(hadoop, spark, storm) +``` + +copyToArray 将 List 中的元素拷贝到数组中指定位置。 + +```scala +object ScalaApp extends App { + + val list = List("hadoop", "spark", "storm") + val array = Array("10", "20", "30") + + list.copyToArray(array,1) + + println(array.toBuffer) +} + +// 输出 :ArrayBuffer(10, hadoop, spark) +``` + +## 六、列表的高级操作 + +### 6.1 列表转换:map & flatMap & foreach + +map 与 Java 8 函数式编程中的 map 类似,都是对 List 中每一个元素执行指定操作。 + +```scala +scala> List(1,2,3).map(_+10) +res15: List[Int] = List(11, 12, 13) +``` + +flatMap 与 map 类似,但如果 List 中的元素还是 List,则会对其进行 flatten 操作。 + +```scala +scala> list.map(_.toList) +res16: List[List[Char]] = List(List(h, a, d, o, o, p), List(s, p, a, r, k), List(s, t, o, r, m)) + +scala> list.flatMap(_.toList) +res17: List[Char] = List(h, a, d, o, o, p, s, p, a, r, k, s, t, o, r, m) +``` + +foreach 要求右侧的操作是一个返回值为 Unit 的函数,你也可以简单理解为执行一段没有返回值代码。 + +```scala +scala> var sum = 0 +sum: Int = 0 + +scala> List(1, 2, 3, 4, 5) foreach (sum += _) + +scala> sum +res19: Int = 15 +``` + +### 6.2 列表过滤:filter & partition & find & takeWhile & dropWhile & span + +filter 用于筛选满足条件元素,返回新的 List。 + +```scala +scala> List(1, 2, 3, 4, 5) filter (_ % 2 == 0) +res20: List[Int] = List(2, 4) +``` + +partition 会按照筛选条件对元素进行分组,返回类型是 tuple(元组)。 + +```scala +scala> List(1, 2, 3, 4, 5) partition (_ % 2 == 0) +res21: (List[Int], List[Int]) = (List(2, 4),List(1, 3, 5)) +``` + +find 查找第一个满足条件的值,由于可能并不存在这样的值,所以返回类型是 `Option`,可以通过 `getOrElse` 在不存在满足条件值的情况下返回默认值。 + +```scala +scala> List(1, 2, 3, 4, 5) find (_ % 2 == 0) +res22: Option[Int] = Some(2) + +val result: Option[Int] = List(1, 2, 3, 4, 5) find (_ % 2 == 0) +result.getOrElse(10) +``` + +takeWhile 遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有遍历到的值。 + +```scala +scala> List(1, 2, 3, -4, 5) takeWhile (_ > 0) +res23: List[Int] = List(1, 2, 3) +``` + +dropWhile 遍历元素,直到遇到第一个不符合条件的值则结束遍历,返回所有未遍历到的值。 + +```scala +// 第一个值就不满足条件,所以返回列表中所有的值 +scala> List(1, 2, 3, -4, 5) dropWhile (_ < 0) +res24: List[Int] = List(1, 2, 3, -4, 5) + + +scala> List(1, 2, 3, -4, 5) dropWhile (_ < 3) +res26: List[Int] = List(3, -4, 5) +``` + +span 遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个 List 中返回,返回类型是 tuple(元组)。 + +```scala +scala> List(1, 2, 3, -4, 5) span (_ > 0) +res27: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5)) +``` + + + +### 6.3 列表检查:forall & exists + +forall 检查 List 中所有元素,如果所有元素都满足条件,则返回 true。 + +```scala +scala> List(1, 2, 3, -4, 5) forall ( _ > 0 ) +res28: Boolean = false +``` + +exists 检查 List 中的元素,如果某个元素已经满足条件,则返回 true。 + +```scala +scala> List(1, 2, 3, -4, 5) exists (_ > 0 ) +res29: Boolean = true +``` + + + +### 6.4 列表排序:sortWith + +sortWith 对 List 中所有元素按照指定规则进行排序,由于 List 是不可变的,所以排序返回一个新的 List。 + +```scala +scala> List(1, -3, 4, 2, 6) sortWith (_ < _) +res30: List[Int] = List(-3, 1, 2, 4, 6) + +scala> val list = List( "hive","spark","azkaban","hadoop") +list: List[String] = List(hive, spark, azkaban, hadoop) + +scala> list.sortWith(_.length>_.length) +res33: List[String] = List(azkaban, hadoop, spark, hive) +``` + + + +## 七、List对象的方法 + +上面介绍的所有方法都是 List 类上的方法,下面介绍的是 List 伴生对象中的方法。 + +### 7.1 List.range + +List.range 可以产生指定的前闭后开区间内的值组成的 List,它有三个可选参数: start(开始值),end(结束值,不包含),step(步长)。 + +```scala +scala> List.range(1, 5) +res34: List[Int] = List(1, 2, 3, 4) + +scala> List.range(1, 9, 2) +res35: List[Int] = List(1, 3, 5, 7) + +scala> List.range(9, 1, -3) +res36: List[Int] = List(9, 6, 3) +``` + +### 7.2 List.fill + +List.fill 使用指定值填充 List。 + +```scala +scala> List.fill(3)("hello") +res37: List[String] = List(hello, hello, hello) + +scala> List.fill(2,3)("world") +res38: List[List[String]] = List(List(world, world, world), List(world, world, world)) +``` + +### 7.3 List.concat + +List.concat 用于拼接多个 List。 + +```scala +scala> List.concat(List('a', 'b'), List('c')) +res39: List[Char] = List(a, b, c) + +scala> List.concat(List(), List('b'), List('c')) +res40: List[Char] = List(b, c) + +scala> List.concat() +res41: List[Nothing] = List() +``` + + + +## 八、处理多个List + +当多个 List 被放入同一个 tuple 中时候,可以通过 zipped 对多个 List 进行关联处理。 + +```scala +// 两个 List 对应位置的元素相乘 +scala> (List(10, 20), List(3, 4, 5)).zipped.map(_ * _) +res42: List[Int] = List(30, 80) + +// 三个 List 的操作也是一样的 +scala> (List(10, 20), List(3, 4, 5), List(100, 200)).zipped.map(_ * _ + _) +res43: List[Int] = List(130, 280) + +// 判断第一个 List 中元素的长度与第二个 List 中元素的值是否相等 +scala> (List("abc", "de"), List(3, 2)).zipped.forall(_.length == _) +res44: Boolean = true +``` + + + +## 九、缓冲列表ListBuffer + +上面介绍的 List,由于其底层实现是链表,这意味着能快速访问 List 头部元素,但对尾部元素的访问则比较低效,这时候可以采用 `ListBuffer`,ListBuffer 提供了在常量时间内往头部和尾部追加元素。 + +```scala +import scala.collection.mutable.ListBuffer + +object ScalaApp extends App { + + val buffer = new ListBuffer[Int] + // 1.在尾部追加元素 + buffer += 1 + buffer += 2 + // 2.在头部追加元素 + 3 +=: buffer + // 3. ListBuffer 转 List + val list: List[Int] = buffer.toList + println(list) +} + +//输出:List(3, 1, 2) +``` + + + +## 十、集(Set) + +Set 是不重复元素的集合。分为可变 Set 和不可变 Set。 + +### 10.1 可变Set + +```scala +object ScalaApp extends App { + + // 可变 Set + val mutableSet = new collection.mutable.HashSet[Int] + + // 1.添加元素 + mutableSet.add(1) + mutableSet.add(2) + mutableSet.add(3) + mutableSet.add(3) + mutableSet.add(4) + + // 2.移除元素 + mutableSet.remove(2) + + // 3.调用 mkString 方法 输出 1,3,4 + println(mutableSet.mkString(",")) + + // 4. 获取 Set 中最小元素 + println(mutableSet.min) + + // 5. 获取 Set 中最大元素 + println(mutableSet.max) + +} +``` + +### 10.2 不可变Set + +不可变 Set 没有 add 方法,可以使用 `+` 添加元素,但是此时会返回一个新的不可变 Set,原来的 Set 不变。 + +```scala +object ScalaApp extends App { + + // 不可变 Set + val immutableSet = new collection.immutable.HashSet[Int] + + val ints: HashSet[Int] = immutableSet+1 + + println(ints) + +} + +// 输出 Set(1) +``` + +### 10.3 Set间操作 + +多个 Set 之间可以进行求交集或者合集等操作。 + +```scala +object ScalaApp extends App { + + // 声明有序 Set + val mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5) + val immutableSet = collection.immutable.SortedSet(3, 4, 5, 6, 7) + + // 两个 Set 的合集 输出:TreeSet(1, 2, 3, 4, 5, 6, 7) + println(mutableSet ++ immutableSet) + + // 两个 Set 的交集 输出:TreeSet(3, 4, 5) + println(mutableSet intersect immutableSet) + +} +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\345\222\214\350\277\220\347\256\227\347\254\246.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\345\222\214\350\277\220\347\256\227\347\254\246.md" new file mode 100644 index 0000000..a303e97 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213\345\222\214\350\277\220\347\256\227\347\254\246.md" @@ -0,0 +1,274 @@ +# Scala基本数据类型和运算符 + + + +## 一、数据类型 + +### 1.1 类型支持 + +Scala 拥有下表所示的数据类型,其中 Byte、Short、Int、Long 和 Char 类型统称为整数类型,整数类型加上 Float 和 Double 统称为数值类型。Scala 数值类型的取值范围和 Java 对应类型的取值范围相同。 + +| 数据类型 | 描述 | +| -------- | ------------------------------------------------------------ | +| Byte | 8 位有符号补码整数。数值区间为 -128 到 127 | +| Short | 16 位有符号补码整数。数值区间为 -32768 到 32767 | +| Int | 32 位有符号补码整数。数值区间为 -2147483648 到 2147483647 | +| Long | 64 位有符号补码整数。数值区间为 -9223372036854775808 到 9223372036854775807 | +| Float | 32 位, IEEE 754 标准的单精度浮点数 | +| Double | 64 位 IEEE 754 标准的双精度浮点数 | +| Char | 16 位无符号 Unicode 字符, 区间值为 U+0000 到 U+FFFF | +| String | 字符序列 | +| Boolean | true 或 false | +| Unit | 表示无值,等同于 Java 中的 void。用作不返回任何结果的方法的结果类型。Unit 只有一个实例值,写成 ()。 | +| Null | null 或空引用 | +| Nothing | Nothing 类型在 Scala 的类层级的最低端;它是任何其他类型的子类型。 | +| Any | Any 是所有其他类的超类 | +| AnyRef | AnyRef 类是 Scala 里所有引用类 (reference class) 的基类 | + +### 1.2 定义变量 + +Scala 的变量分为两种,val 和 var,其区别如下: + ++ **val** : 类似于 Java 中的 final 变量,一旦初始化就不能被重新赋值; ++ **var** :类似于 Java 中的非 final 变量,在整个声明周期内 var 可以被重新赋值; + +```scala +scala> val a=1 +a: Int = 1 + +scala> a=2 +:8: error: reassignment to val // 不允许重新赋值 + +scala> var b=1 +b: Int = 1 + +scala> b=2 +b: Int = 2 +``` + +### 1.3 类型推断 + +在上面的演示中,并没有声明 a 是 Int 类型,但是程序还是把 a 当做 Int 类型,这就是 Scala 的类型推断。在大多数情况下,你都无需指明变量的类型,程序会自动进行推断。如果你想显式的声明类型,可以在变量后面指定,如下: + +```scala +scala> val c:String="hello scala" +c: String = hello scala +``` + +### 1.4 Scala解释器 + +在 scala 命令行中,如果没有对输入的值指定赋值的变量,则输入的值默认会赋值给 `resX`(其中 X 是一个从 0 开始递增的整数),`res` 是 result 的缩写,这个变量可以在后面的语句中进行引用。 + +```scala +scala> 5 +res0: Int = 5 + +scala> res0*6 +res1: Int = 30 + +scala> println(res1) +30 +``` + + + +## 二、字面量 + +Scala 和 Java 字面量在使用上很多相似,比如都使用 F 或 f 表示浮点型,都使用 L 或 l 表示 Long 类型。下文主要介绍两者差异部分。 + +```scala +scala> 1.2 +res0: Double = 1.2 + +scala> 1.2f +res1: Float = 1.2 + +scala> 1.4F +res2: Float = 1.4 + +scala> 1 +res3: Int = 1 + +scala> 1l +res4: Long = 1 + +scala> 1L +res5: Long = 1 +``` + +### 2.1 整数字面量 + +Scala 支持 10 进制和 16 进制,但不支持八进制字面量和以 0 开头的整数字面量。 + +```scala +scala> 012 +:1: error: Decimal integer literals may not have a leading zero. (Octal syntax is obsolete.) +``` + +### 2.2 字符串字面量 + +#### 1. 字符字面量 + +字符字面量由一对单引号和中间的任意 Unicode 字符组成。你可以显式的给出原字符、也可以使用字符的 Unicode 码来表示,还可以包含特殊的转义字符。 + +```scala +scala> '\u0041' +res0: Char = A + +scala> 'a' +res1: Char = a + +scala> '\n' +res2: Char = +``` + +#### 2. 字符串字面量 + +字符串字面量由双引号包起来的字符组成。 + +```scala +scala> "hello world" +res3: String = hello world +``` + +#### 3.原生字符串 + +Scala 提供了 `""" ... """` 语法,通过三个双引号来表示原生字符串和多行字符串,使用该种方式,原生字符串中的特殊字符不会被转义。 + +```scala +scala> "hello \tool" +res4: String = hello ool + +scala> """hello \tool""" +res5: String = hello \tool + +scala> """hello + | world""" +res6: String = +hello +world +``` + +### 2.3 符号字面量 + +符号字面量写法为: `'标识符 ` ,这里 标识符可以是任何字母或数字的组合。符号字面量会被映射成 `scala.Symbol` 的实例,如:符号字面量 `'x ` 会被编译器翻译为 `scala.Symbol("x")`。符号字面量可选方法很少,只能通过 `.name` 获取其名称。 + +注意:具有相同 `name` 的符号字面量一定指向同一个 Symbol 对象,不同 `name` 的符号字面量一定指向不同的 Symbol 对象。 + +```scala +scala> val sym = 'ID008 +sym: Symbol = 'ID008 + +scala> sym.name +res12: String = ID008 +``` + +### 2.4 插值表达式 + +Scala 支持插值表达式。 + +```scala +scala> val name="xiaoming" +name: String = xiaoming + +scala> println(s"My name is $name,I'm ${2*9}.") +My name is xiaoming,I'm 18. +``` + +## 三、运算符 + +Scala 和其他语言一样,支持大多数的操作运算符: + +- 算术运算符(+,-,*,/,%) +- 关系运算符(==,!=,>,<,>=,<=) +- 逻辑运算符 (&&,||,!,&,|) +- 位运算符 (~,&,|,^,<<,>>,>>>) +- 赋值运算符 (=,+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=) + +以上操作符的基本使用与 Java 类似,下文主要介绍差异部分和注意事项。 + +### 3.1 运算符即方法 + +Scala 的面向对象比 Java 更加纯粹,在 Scala 中一切都是对象。所以对于 `1+2`,实际上是调用了 Int 类中名为 `+` 的方法,所以 1+2,也可以写成 `1.+(2)`。 + +```scala +scala> 1+2 +res14: Int = 3 + +scala> 1.+(2) +res15: Int = 3 +``` + +Int 类中包含了多个重载的 `+` 方法,用于分别接收不同类型的参数。 + +
+ +### 3.2 逻辑运算符 + +和其他语言一样,在 Scala 中 `&&`,`||` 的执行是短路的,即如果左边的表达式能确定整个结果,右边的表达式就不会被执行,这满足大多数使用场景。但是如果你需要在无论什么情况下,都执行右边的表达式,则可以使用 `&` 或 `|` 代替。 + +### 3.3 赋值运算符 + +在 Scala 中没有 Java 中的 `++` 和 `--` 运算符,如果你想要实现类似的操作,只能使用 `+=1`,或者 `-=1`。 + +```scala +scala> var a=1 +a: Int = 1 + +scala> a+=1 + +scala> a +res8: Int = 2 + +scala> a-=1 + +scala> a +res10: Int = 1 +``` + +### 3.4 运算符优先级 + +操作符的优先级如下:优先级由上至下,逐级递减。 + +
+ +在表格中某个字符的优先级越高,那么以这个字符打头的方法就拥有更高的优先级。如 `+` 的优先级大于 `<`,也就意味则 `+` 的优先级大于以 `<` 开头的 `<<`,所以 `2<<2+2` , 实际上等价于 `2<<(2+2)` : + +```scala +scala> 2<<2+2 +res0: Int = 32 + +scala> 2<<(2+2) +res1: Int = 32 +``` + +### 3.5 对象相等性 + +如果想要判断两个对象是否相等,可以使用 `==` 和 `!=`,这两个操作符可以用于所有的对象,包括 null。 + +```scala +scala> 1==2 +res2: Boolean = false + +scala> List(1,2,3)==List(1,2,3) +res3: Boolean = true + +scala> 1==1.0 +res4: Boolean = true + +scala> List(1,2,3)==null +res5: Boolean = false + +scala> null==null +res6: Boolean = true +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\225\260\347\273\204.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\225\260\347\273\204.md" new file mode 100644 index 0000000..d7215a8 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\225\260\347\273\204.md" @@ -0,0 +1,193 @@ +# Scala 数组相关操作 + + + +## 一、定长数组 + +在 Scala 中,如果你需要一个长度不变的数组,可以使用 Array。但需要注意以下两点: + +- 在 Scala 中使用 `(index)` 而不是 `[index]` 来访问数组中的元素,因为访问元素,对于 Scala 来说是方法调用,`(index)` 相当于执行了 `.apply(index)` 方法。 +- Scala 中的数组与 Java 中的是等价的,`Array[Int]()` 在虚拟机层面就等价于 Java 的 `int[]`。 + +```scala +// 10 个整数的数组,所有元素初始化为 0 +scala> val nums=new Array[Int](10) +nums: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + +// 10 个元素的字符串数组,所有元素初始化为 null +scala> val strings=new Array[String](10) +strings: Array[String] = Array(null, null, null, null, null, null, null, null, null, null) + +// 使用指定值初始化,此时不需要 new 关键字 +scala> val a=Array("hello","scala") +a: Array[String] = Array(hello, scala) + +// 使用 () 来访问元素 +scala> a(0) +res3: String = hello +``` + +## 二、变长数组 + +在 scala 中通过 ArrayBuffer 实现变长数组 (又称缓冲数组)。在构建 ArrayBuffer 时必须给出类型参数,但不必指定长度,因为 ArrayBuffer 会在需要的时候自动扩容和缩容。变长数组的构建方式及常用操作如下: + +```java +import scala.collection.mutable.ArrayBuffer + +object ScalaApp { + + // 相当于 Java 中的 main 方法 + def main(args: Array[String]): Unit = { + // 1.声明变长数组 (缓冲数组) + val ab = new ArrayBuffer[Int]() + // 2.在末端增加元素 + ab += 1 + // 3.在末端添加多个元素 + ab += (2, 3, 4) + // 4.可以使用 ++=追加任何集合 + ab ++= Array(5, 6, 7) + // 5.缓冲数组可以直接打印查看 + println(ab) + // 6.移除最后三个元素 + ab.trimEnd(3) + // 7.在第 1 个元素之后插入多个新元素 + ab.insert(1, 8, 9) + // 8.从第 2 个元素开始,移除 3 个元素,不指定第二个参数的话,默认值为 1 + ab.remove(2, 3) + // 9.缓冲数组转定长数组 + val abToA = ab.toArray + // 10. 定长数组打印为其 hashcode 值 + println(abToA) + // 11. 定长数组转缓冲数组 + val aToAb = abToA.toBuffer + } +} +``` + +需要注意的是:使用 `+= ` 在末尾插入元素是一个高效的操作,其时间复杂度是 O(1)。而使用 `insert` 随机插入元素的时间复杂度是 O(n),因为在其插入位置之后的所有元素都要进行对应的后移,所以在 `ArrayBuffer` 中随机插入元素是一个低效的操作。 + +## 三、数组遍历 + +```scala +object ScalaApp extends App { + + val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + // 1.方式一 相当于 Java 中的增强 for 循环 + for (elem <- a) { + print(elem) + } + + // 2.方式二 + for (index <- 0 until a.length) { + print(a(index)) + } + + // 3.方式三, 是第二种方式的简写 + for (index <- a.indices) { + print(a(index)) + } + + // 4.反向遍历 + for (index <- a.indices.reverse) { + print(a(index)) + } + +} +``` + +这里我们没有将代码写在 main 方法中,而是继承自 App.scala,这是 Scala 提供的一种简写方式,此时将代码写在类中,等价于写在 main 方法中,直接运行该类即可。 + + + +## 四、数组转换 + +数组转换是指由现有数组产生新的数组。假设当前拥有 a 数组,想把 a 中的偶数元素乘以 10 后产生一个新的数组,可以采用下面两种方式来实现: + +```scala +object ScalaApp extends App { + + val a = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + + // 1.方式一 yield 关键字 + val ints1 = for (elem <- a if elem % 2 == 0) yield 10 * elem + for (elem <- ints1) { + println(elem) + } + + // 2.方式二 采用函数式编程的方式,这和 Java 8 中的函数式编程是类似的,这里采用下划线标表示其中的每个元素 + val ints2 = a.filter(_ % 2 == 0).map(_ * 10) + for (elem <- ints1) { + println(elem) + } +} +``` + + + +## 五、多维数组 + +和 Java 中一样,多维数组由单维数组组成。 + +```scala +object ScalaApp extends App { + + val matrix = Array(Array(11, 12, 13, 14, 15, 16, 17, 18, 19, 20), + Array(21, 22, 23, 24, 25, 26, 27, 28, 29, 30), + Array(31, 32, 33, 34, 35, 36, 37, 38, 39, 40)) + + + for (elem <- matrix) { + + for (elem <- elem) { + print(elem + "-") + } + println() + } + +} + +打印输出如下: +11-12-13-14-15-16-17-18-19-20- +21-22-23-24-25-26-27-28-29-30- +31-32-33-34-35-36-37-38-39-40- +``` + + + +## 六、与Java互操作 + +由于 Scala 的数组是使用 Java 的数组来实现的,所以两者之间可以相互转换。 + +```scala +import java.util + +import scala.collection.mutable.ArrayBuffer +import scala.collection.{JavaConverters, mutable} + +object ScalaApp extends App { + + val element = ArrayBuffer("hadoop", "spark", "storm") + // Scala 转 Java + val javaList: util.List[String] = JavaConverters.bufferAsJavaList(element) + // Java 转 Scala + val scalaBuffer: mutable.Buffer[String] = JavaConverters.asScalaBuffer(javaList) + for (elem <- scalaBuffer) { + println(elem) + } +} +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\230\240\345\260\204\345\222\214\345\205\203\347\273\204.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\230\240\345\260\204\345\222\214\345\205\203\347\273\204.md" new file mode 100644 index 0000000..ec47e12 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\230\240\345\260\204\345\222\214\345\205\203\347\273\204.md" @@ -0,0 +1,282 @@ +# Map & Tuple + + + + + +## 一、映射(Map) + +### 1.1 构造Map + +```scala +// 初始化一个空 map +val scores01 = new HashMap[String, Int] + +// 从指定的值初始化 Map(方式一) +val scores02 = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + +// 从指定的值初始化 Map(方式二) +val scores03 = Map(("hadoop", 10), ("spark", 20), ("storm", 30)) +``` + +采用上面方式得到的都是不可变 Map(immutable map),想要得到可变 Map(mutable map),则需要使用: + +```scala +val scores04 = scala.collection.mutable.Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) +``` + +### 1.2 获取值 + +```scala +object ScalaApp extends App { + + val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + // 1.获取指定 key 对应的值 + println(scores("hadoop")) + + // 2. 如果对应的值不存在则使用默认值 + println(scores.getOrElse("hadoop01", 100)) +} +``` + +### 1.3 新增/修改/删除值 + +可变 Map 允许进行新增、修改、删除等操作。 + +```scala +object ScalaApp extends App { + + val scores = scala.collection.mutable.Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + // 1.如果 key 存在则更新 + scores("hadoop") = 100 + + // 2.如果 key 不存在则新增 + scores("flink") = 40 + + // 3.可以通过 += 来进行多个更新或新增操作 + scores += ("spark" -> 200, "hive" -> 50) + + // 4.可以通过 -= 来移除某个键和值 + scores -= "storm" + + for (elem <- scores) {println(elem)} +} + +// 输出内容如下 +(spark,200) +(hadoop,100) +(flink,40) +(hive,50) +``` + +不可变 Map 不允许进行新增、修改、删除等操作,但是允许由不可变 Map 产生新的 Map。 + +```scala +object ScalaApp extends App { + + val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + val newScores = scores + ("spark" -> 200, "hive" -> 50) + + for (elem <- scores) {println(elem)} + +} + +// 输出内容如下 +(hadoop,10) +(spark,200) +(storm,30) +(hive,50) +``` + +### 1.4 遍历Map + +```java +object ScalaApp extends App { + + val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + // 1. 遍历键 + for (key <- scores.keys) { println(key) } + + // 2. 遍历值 + for (value <- scores.values) { println(value) } + + // 3. 遍历键值对 + for ((key, value) <- scores) { println(key + ":" + value) } + +} +``` + +### 1.5 yield关键字 + +可以使用 `yield` 关键字从现有 Map 产生新的 Map。 + +```scala +object ScalaApp extends App { + + val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + // 1.将 scores 中所有的值扩大 10 倍 + val newScore = for ((key, value) <- scores) yield (key, value * 10) + for (elem <- newScore) { println(elem) } + + + // 2.将键和值互相调换 + val reversalScore: Map[Int, String] = for ((key, value) <- scores) yield (value, key) + for (elem <- reversalScore) { println(elem) } + +} + +// 输出 +(hadoop,100) +(spark,200) +(storm,300) + +(10,hadoop) +(20,spark) +(30,storm) +``` + +### 1.6 其他Map结构 + +在使用 Map 时候,如果不指定,默认使用的是 HashMap,如果想要使用 `TreeMap` 或者 `LinkedHashMap`,则需要显式的指定。 + +```scala +object ScalaApp extends App { + + // 1.使用 TreeMap,按照键的字典序进行排序 + val scores01 = scala.collection.mutable.TreeMap("B" -> 20, "A" -> 10, "C" -> 30) + for (elem <- scores01) {println(elem)} + + // 2.使用 LinkedHashMap,按照键值对的插入顺序进行排序 + val scores02 = scala.collection.mutable.LinkedHashMap("B" -> 20, "A" -> 10, "C" -> 30) + for (elem <- scores02) {println(elem)} +} + +// 输出 +(A,10) +(B,20) +(C,30) + +(B,20) +(A,10) +(C,30) +``` + +### 1.7 可选方法 + +```scala +object ScalaApp extends App { + + val scores = scala.collection.mutable.TreeMap("B" -> 20, "A" -> 10, "C" -> 30) + + // 1. 获取长度 + println(scores.size) + + // 2. 判断是否为空 + println(scores.isEmpty) + + // 3. 判断是否包含特定的 key + println(scores.contains("A")) + +} +``` + +### 1.8 与Java互操作 + +```scala +import java.util +import scala.collection.{JavaConverters, mutable} + +object ScalaApp extends App { + + val scores = Map("hadoop" -> 10, "spark" -> 20, "storm" -> 30) + + // scala map 转 java map + val javaMap: util.Map[String, Int] = JavaConverters.mapAsJavaMap(scores) + + // java map 转 scala map + val scalaMap: mutable.Map[String, Int] = JavaConverters.mapAsScalaMap(javaMap) + + for (elem <- scalaMap) {println(elem)} +} +``` + + + +## 二、元组(Tuple) + +元组与数组类似,但是数组中所有的元素必须是同一种类型,而元组则可以包含不同类型的元素。 + +```scala +scala> val tuple=(1,3.24f,"scala") +tuple: (Int, Float, String) = (1,3.24,scala) +``` + +### 2.1 模式匹配 + +可以通过模式匹配来获取元组中的值并赋予对应的变量: + +```scala +scala> val (a,b,c)=tuple +a: Int = 1 +b: Float = 3.24 +c: String = scala +``` + +如果某些位置不需要赋值,则可以使用下划线代替: + +```scala +scala> val (a,_,_)=tuple +a: Int = 1 +``` + +### 2.2 zip方法 + +```scala +object ScalaApp extends App { + + val array01 = Array("hadoop", "spark", "storm") + val array02 = Array(10, 20, 30) + + // 1.zip 方法得到的是多个 tuple 组成的数组 + val tuples: Array[(String, Int)] = array01.zip(array02) + // 2.也可以在 zip 后调用 toMap 方法转换为 Map + val map: Map[String, Int] = array01.zip(array02).toMap + + for (elem <- tuples) { println(elem) } + for (elem <- map) {println(elem)} +} + +// 输出 +(hadoop,10) +(spark,20) +(storm,30) + +(hadoop,10) +(spark,20) +(storm,30) +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\250\241\345\274\217\345\214\271\351\205\215.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\250\241\345\274\217\345\214\271\351\205\215.md" new file mode 100644 index 0000000..7c65267 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\250\241\345\274\217\345\214\271\351\205\215.md" @@ -0,0 +1,172 @@ +# Scala模式匹配 + + + +## 一、模式匹配 + +Scala 支持模式匹配机制,可以代替 swith 语句、执行类型检查、以及支持析构表达式等。 + +### 1.1 更好的swith + +Scala 不支持 swith,可以使用模式匹配 `match...case` 语法代替。但是 match 语句与 Java 中的 switch 有以下三点不同: + +- Scala 中的 case 语句支持任何类型;而 Java 中 case 语句仅支持整型、枚举和字符串常量; +- Scala 中每个分支语句后面不需要写 break,因为在 case 语句中 break 是隐含的,默认就有; +- 在 Scala 中 match 语句是有返回值的,而 Java 中 switch 语句是没有返回值的。如下: + +```scala +object ScalaApp extends App { + + def matchTest(x: Int) = x match { + case 1 => "one" + case 2 => "two" + case _ if x > 9 && x < 100 => "两位数" //支持条件表达式 这被称为模式守卫 + case _ => "other" + } + + println(matchTest(1)) //输出 one + println(matchTest(10)) //输出 两位数 + println(matchTest(200)) //输出 other +} +``` + +### 1.2 用作类型检查 + +```scala +object ScalaApp extends App { + + def matchTest[T](x: T) = x match { + case x: Int => "数值型" + case x: String => "字符型" + case x: Float => "浮点型" + case _ => "other" + } + + println(matchTest(1)) //输出 数值型 + println(matchTest(10.3f)) //输出 浮点型 + println(matchTest("str")) //输出 字符型 + println(matchTest(2.1)) //输出 other +} +``` + +### 1.3 匹配数据结构 + +匹配元组示例: + +```scala +object ScalaApp extends App { + + def matchTest(x: Any) = x match { + case (0, _, _) => "匹配第一个元素为 0 的元组" + case (a, b, c) => println(a + "~" + b + "~" + c) + case _ => "other" + } + + println(matchTest((0, 1, 2))) // 输出: 匹配第一个元素为 0 的元组 + matchTest((1, 2, 3)) // 输出: 1~2~3 + println(matchTest(Array(10, 11, 12, 14))) // 输出: other +} +``` + +匹配数组示例: + +```scala +object ScalaApp extends App { + + def matchTest[T](x: Array[T]) = x match { + case Array(0) => "匹配只有一个元素 0 的数组" + case Array(a, b) => println(a + "~" + b) + case Array(10, _*) => "第一个元素为 10 的数组" + case _ => "other" + } + + println(matchTest(Array(0))) // 输出: 匹配只有一个元素 0 的数组 + matchTest(Array(1, 2)) // 输出: 1~2 + println(matchTest(Array(10, 11, 12))) // 输出: 第一个元素为 10 的数组 + println(matchTest(Array(3, 2, 1))) // 输出: other +} +``` + +### 1.4 提取器 + +数组、列表和元组能使用模式匹配,都是依靠提取器 (extractor) 机制,它们伴生对象中定义了 `unapply` 或 `unapplySeq` 方法: + ++ **unapply**:用于提取固定数量的对象; ++ **unapplySeq**:用于提取一个序列; + +这里以数组为例,`Array.scala` 定义了 `unapplySeq` 方法: + +```scala +def unapplySeq[T](x : scala.Array[T]) : scala.Option[scala.IndexedSeq[T]] = { /* compiled code */ } +``` + +`unapplySeq` 返回一个序列,包含数组中的所有值,这样在模式匹配时,才能知道对应位置上的值。 + + + +## 二、样例类 + +### 2.1 样例类 + +样例类是一种的特殊的类,它们被经过优化以用于模式匹配,样例类的声明比较简单,只需要在 `class` 前面加上关键字 `case`。下面给出一个样例类及其用于模式匹配的示例: + +```scala +//声明一个抽象类 +abstract class Person{} +``` + +```scala +// 样例类 Employee +case class Employee(name: String, age: Int, salary: Double) extends Person {} +``` + +```scala +// 样例类 Student +case class Student(name: String, age: Int) extends Person {} +``` + +当你声明样例类后,编译器自动进行以下配置: + +- 构造器中每个参数都默认为 `val`; +- 自动地生成 `equals, hashCode, toString, copy` 等方法; +- 伴生对象中自动生成 `apply` 方法,使得可以不用 new 关键字就能构造出相应的对象; +- 伴生对象中自动生成 `unapply` 方法,以支持模式匹配。 + +除了上面的特征外,样例类和其他类相同,可以任意添加方法和字段,扩展它们。 + +### 2.3 用于模式匹配 + +样例的伴生对象中自动生成 `unapply` 方法,所以样例类可以支持模式匹配,使用如下: + +```scala +object ScalaApp extends App { + + def matchTest(person: Person) = person match { + case Student(name, _) => "student:" + name + case Employee(_, _, salary) => "employee salary:" + salary + case _ => "other" + } + + println(matchTest(Student("heibai", 12))) //输出: student:heibai + println(matchTest(Employee("ying", 22, 999999))) //输出: employee salary:999999.0 +} +``` + + + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\265\201\347\250\213\346\216\247\345\210\266\350\257\255\345\217\245.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\265\201\347\250\213\346\216\247\345\210\266\350\257\255\345\217\245.md" new file mode 100644 index 0000000..8b1c219 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\346\265\201\347\250\213\346\216\247\345\210\266\350\257\255\345\217\245.md" @@ -0,0 +1,211 @@ +# 流程控制语句 + + + +## 一、条件表达式if + +Scala 中的 if/else 语法结构与 Java 中的一样,唯一不同的是,Scala 中的 if 表达式是有返回值的。 + +```scala +object ScalaApp extends App { + + val x = "scala" + val result = if (x.length == 5) "true" else "false" + print(result) + +} +``` + +在 Java 中,每行语句都需要使用 `;` 表示结束,但是在 Scala 中并不需要。除非你在单行语句中写了多行代码。 + + + +## 二、块表达式 + +在 Scala 中,可以使用 `{}` 块包含一系列表达式,块中最后一个表达式的值就是块的值。 + +```scala +object ScalaApp extends App { + + val result = { + val a = 1 + 1; val b = 2 + 2; a + b + } + print(result) +} + +// 输出: 6 +``` + +如果块中的最后一个表达式没有返回值,则块的返回值是 Unit 类型。 + +```scala +scala> val result ={ val a = 1 + 1; val b = 2 + 2 } +result: Unit = () +``` + + + +## 三、循环表达式while + +Scala 和大多数语言一样,支持 `while` 和 `do ... while` 表达式。 + +```scala +object ScalaApp extends App { + + var n = 0 + + while (n < 10) { + n += 1 + println(n) + } + + // 循环至少要执行一次 + do { + println(n) + } while (n > 10) +} +``` + + + +## 四、循环表达式for + +for 循环的基本使用如下: + +```scala +object ScalaApp extends App { + + // 1.基本使用 输出[1,9) + for (n <- 1 until 10) {print(n)} + + // 2.使用多个表达式生成器 输出: 11 12 13 21 22 23 31 32 33 + for (i <- 1 to 3; j <- 1 to 3) print(f"${10 * i + j}%3d") + + // 3.使用带条件的表达式生成器 输出: 12 13 21 23 31 32 + for (i <- 1 to 3; j <- 1 to 3 if i != j) print(f"${10 * i + j}%3d") + +} +``` + +除了基本使用外,还可以使用 `yield` 关键字从 for 循环中产生 Vector,这称为 for 推导式。 + +```scala +scala> for (i <- 1 to 10) yield i * 6 +res1: scala.collection.immutable.IndexedSeq[Int] = Vector(6, 12, 18, 24, 30, 36, 42, 48, 54, 60) +``` + + + +## 五、异常处理try + +和 Java 中一样,支持 `try...catch...finally` 语句。 + +```scala +import java.io.{FileNotFoundException, FileReader} + +object ScalaApp extends App { + + try { + val reader = new FileReader("wordCount.txt") + } catch { + case ex: FileNotFoundException => + ex.printStackTrace() + println("没有找到对应的文件!") + } finally { + println("finally 语句一定会被执行!") + } +} +``` + +这里需要注意的是因为 finally 语句一定会被执行,所以不要在该语句中返回值,否则返回值会被作为整个 try 语句的返回值,如下: + +```scala +scala> def g():Int = try return 1 finally return 2 +g: ()Int + +// 方法 g() 总会返回 2 +scala> g() +res3: Int = 2 +``` + + + +## 六、条件选择表达式match + +match 类似于 java 中的 switch 语句。 + +```scala +object ScalaApp extends App { + + val elements = Array("A", "B", "C", "D", "E") + + for (elem <- elements) { + elem match { + case "A" => println(10) + case "B" => println(20) + case "C" => println(30) + case _ => println(50) + } + } +} + +``` + +但是与 Java 中的 switch 有以下三点不同: + ++ Scala 中的 case 语句支持任何类型;而 Java 中 case 语句仅支持整型、枚举和字符串常量; ++ Scala 中每个分支语句后面不需要写 break,因为在 case 语句中 break 是隐含的,默认就有; ++ 在 Scala 中 match 语句是有返回值的,而 Java 中 switch 语句是没有返回值的。如下: + +```scala +object ScalaApp extends App { + + val elements = Array("A", "B", "C", "D", "E") + + for (elem <- elements) { + val score = elem match { + case "A" => 10 + case "B" => 20 + case "C" => 30 + case _ => 50 + } + print(elem + ":" + score + ";") + } +} +// 输出: A:10;B:20;C:30;D:50;E:50; +``` + + + +## 七、没有break和continue + +额外注意一下:Scala 中并不支持 Java 中的 break 和 continue 关键字。 + + + +## 八、输入与输出 + +在 Scala 中可以使用 print、println、printf 打印输出,这与 Java 中是一样的。如果需要从控制台中获取输入,则可以使用 `StdIn` 中定义的各种方法。 + +```scala +val name = StdIn.readLine("Your name: ") +print("Your age: ") +val age = StdIn.readInt() +println(s"Hello, ${name}! Next year, you will be ${age + 1}.") +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\256\200\344\273\213\345\217\212\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\256\200\344\273\213\345\217\212\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256.md" new file mode 100644 index 0000000..003beda --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\256\200\344\273\213\345\217\212\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256.md" @@ -0,0 +1,133 @@ +# Scala简介及开发环境配置 + + + + +## 一、Scala简介 + +### 1.1 概念 + +Scala 全称为 Scalable Language,即“可伸缩的语言”,之所以这样命名,是因为它的设计目标是希望伴随着用户的需求一起成长。Scala 是一门综合了**面向对象**和**函数式编程概念**的**静态类型**的编程语言,它运行在标准的 Java 平台上,可以与所有的 Java 类库无缝协作。 + + + +### 1.2 特点 + +#### 1. Scala是面向对象的 + +Scala 是一种面向对象的语言,每个值都是对象,每个方法都是调用。举例来说,如果你执行 `1+2`,则对于 Scala 而言,实际是在调用 Int 类里定义的名为 `+` 的方法。 + +#### 2. Scala是函数式的 + +Scala 不只是一门纯的面对对象的语言,它也是功能完整的函数式编程语言。函数式编程以两大核心理念为指导: + ++ 函数是一等公民; ++ 程序中的操作应该将输入值映射成输出值,而不是当场修改数据。即方法不应该有副作用。 + + + +### 1.3 Scala的优点 + +#### 1. 与Java的兼容 + +Scala 可以与 Java 无缝对接,其在执行时会被编译成 JVM 字节码,这使得其性能与 Java 相当。Scala 可以直接调用 Java 中的方法、访问 Java 中的字段、继承 Java 类、实现 Java 接口。Scala 重度复用并包装了原生的 Java 类型,并支持隐式转换。 + +#### 2. 精简的语法 + +Scala 的程序通常比较简洁,相比 Java 而言,代码行数会大大减少,这使得程序员对代码的阅读和理解更快,缺陷也更少。 + +#### 3. 高级语言的特性 + +Scala 具有高级语言的特定,对代码进行了高级别的抽象,能够让你更好地控制程序的复杂度,保证开发的效率。 + +#### 4. 静态类型 + +Scala 拥有非常先进的静态类型系统,Scala 不仅拥有与 Java 类似的允许嵌套类的类型系统,还支持使用泛型对类型进行参数化,用交集(intersection)来组合类型,以及使用抽象类型来进行隐藏类型的细节。通过这些特性,可以更快地设计出安全易用的程序和接口。 + + + + + +## 二、配置IDEA开发环境 + +### 2.1 前置条件 + +Scala 的运行依赖于 JDK,Scala 2.12.x 需要 JDK 1.8+。 + +### 2.2 安装Scala插件 + +IDEA 默认不支持 Scala 语言的开发,需要通过插件进行扩展。打开 IDEA,依次点击 **File** => **settings**=> **plugins** 选项卡,搜索 Scala 插件 (如下图)。找到插件后进行安装,并重启 IDEA 使得安装生效。 + +
+ + + +### 2.3 创建Scala项目 + +在 IDEA 中依次点击 **File** => **New** => **Project** 选项卡,然后选择创建 `Scala—IDEA` 工程: + +
+ + + +### 2.4 下载Scala SDK + +#### 1. 方式一 + +此时看到 `Scala SDK` 为空,依次点击 `Create` => `Download` ,选择所需的版本后,点击 `OK` 按钮进行下载,下载完成点击 `Finish` 进入工程。 + +
+ + + +#### 2. 方式二 + +方式一是 Scala 官方安装指南里使用的方式,但下载速度通常比较慢,且这种安装下并没有直接提供 Scala 命令行工具。所以个人推荐到官网下载安装包进行安装,下载地址:https://www.scala-lang.org/download/ + +这里我的系统是 Windows,下载 msi 版本的安装包后,一直点击下一步进行安装,安装完成后会自动配置好环境变量。 + +
+ + + +由于安装时已经自动配置好环境变量,所以 IDEA 会自动选择对应版本的 SDK。 + +
+ + + +### 2.5 创建Hello World + +在工程 `src` 目录上右击 **New** => **Scala class** 创建 `Hello.scala`。输入代码如下,完成后点击运行按钮,成功运行则代表搭建成功。 + +
+ + + + + +### 2.6 切换Scala版本 + +在日常的开发中,由于对应软件(如 Spark)的版本切换,可能导致需要切换 Scala 的版本,则可以在 `Project Structures` 中的 `Global Libraries` 选项卡中进行切换。 + +
+ + + + + +### 2.7 使用scala命令行 + +采用 `msi` 方式安装,程序会自动配置好环境变量。此时可以直接使用命令行工具: + +
+ + + +## 参考资料 + +1. Martin Odersky(著),高宇翔 (译) . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. https://www.scala-lang.org/download/ diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\222\214\345\257\271\350\261\241.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\222\214\345\257\271\350\261\241.md" new file mode 100644 index 0000000..881ebe2 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\222\214\345\257\271\350\261\241.md" @@ -0,0 +1,412 @@ +# 类和对象 + + + +## 一、初识类和对象 + +Scala 的类与 Java 的类具有非常多的相似性,示例如下: + +```scala +// 1. 在 scala 中,类不需要用 public 声明,所有的类都具有公共的可见性 +class Person { + + // 2. 声明私有变量,用 var 修饰的变量默认拥有 getter/setter 属性 + private var age = 0 + + // 3.如果声明的变量不需要进行初始赋值,此时 Scala 就无法进行类型推断,所以需要显式指明类型 + private var name: String = _ + + + // 4. 定义方法,应指明传参类型。返回值类型不是必须的,Scala 可以自动推断出来,但是为了方便调用者,建议指明 + def growUp(step: Int): Unit = { + age += step + } + + // 5.对于改值器方法 (即改变对象状态的方法),即使不需要传入参数,也建议在声明中包含 () + def growUpFix(): Unit = { + age += 10 + } + + // 6.对于取值器方法 (即不会改变对象状态的方法),不必在声明中包含 () + def currentAge: Int = { + age + } + + /** + * 7.不建议使用 return 关键字,默认方法中最后一行代码的计算结果为返回值 + * 如果方法很简短,甚至可以写在同一行中 + */ + def getName: String = name + +} + + +// 伴生对象 +object Person { + + def main(args: Array[String]): Unit = { + // 8.创建类的实例 + val counter = new Person() + // 9.用 var 修饰的变量默认拥有 getter/setter 属性,可以直接对其进行赋值 + counter.age = 12 + counter.growUp(8) + counter.growUpFix() + // 10.用 var 修饰的变量默认拥有 getter/setter 属性,可以直接对其进行取值,输出: 30 + println(counter.age) + // 输出: 30 + println(counter.currentAge) + // 输出: null + println(counter.getName) + } + +} +``` + +
+ +## 二、类 + +### 2.1 成员变量可见性 + +Scala 中成员变量的可见性默认都是 public,如果想要保证其不被外部干扰,可以声明为 private,并通过 getter 和 setter 方法进行访问。 + +### 2.2 getter和setter属性 + +getter 和 setter 属性与声明变量时使用的关键字有关: + ++ 使用 var 关键字:变量同时拥有 getter 和 setter 属性; ++ 使用 val 关键字:变量只拥有 getter 属性; ++ 使用 private[this]:变量既没有 getter 属性、也没有 setter 属性,只能通过内部的方法访问; + +需要特别说明的是:假设变量名为 age,则其对应的 get 和 set 的方法名分别叫做 ` age` 和 `age_=`。 + +```scala +class Person { + + private val name = "heibaiying" + private var age = 12 + private[this] var birthday = "2019-08-08" + // birthday 只能被内部方法所访问 + def getBirthday: String = birthday +} + +object Person { + def main(args: Array[String]): Unit = { + val person = new Person + person.age = 30 + println(person.name) + println(person.age) + println(person.getBirthday) + } +} +``` + +> 解释说明: +> +> 示例代码中 `person.age=30` 在执行时内部实际是调用了方法 `person.age_=(30) `,而 `person.age` 内部执行时实际是调用了 `person.age()` 方法。想要证明这一点,可以对代码进行反编译。同时为了说明成员变量可见性的问题,我们对下面这段代码进行反编译: +> +> ```scala +> class Person { +> var name = "" +> private var age = "" +> } +> ``` +> +> 依次执行下面编译命令: +> +> ```shell +> > scalac Person.scala +> > javap -private Person +> ``` +> +> 编译结果如下,从编译结果可以看到实际的 get 和 set 的方法名 (因为 JVM 不允许在方法名中出现=,所以它被翻译成$eq),同时也验证了成员变量默认的可见性为 public。 +> +> ```java +> Compiled from "Person.scala" +> public class Person { +> private java.lang.String name; +> private java.lang.String age; +> +> public java.lang.String name(); +> public void name_$eq(java.lang.String); +> +> private java.lang.String age(); +> private void age_$eq(java.lang.String); +> +> public Person(); +> } +> ``` + +### 2.3 @BeanProperty + +在上面的例子中可以看到我们是使用 `.` 来对成员变量进行访问的,如果想要额外生成和 Java 中一样的 getXXX 和 setXXX 方法,则需要使用@BeanProperty 进行注解。 + +```scala +class Person { + @BeanProperty var name = "" +} + +object Person { + def main(args: Array[String]): Unit = { + val person = new Person + person.setName("heibaiying") + println(person.getName) + } +} +``` + + + +### 2.4 主构造器 + +和 Java 不同的是,Scala 类的主构造器直接写在类名后面,但注意以下两点: + ++ 主构造器传入的参数默认就是 val 类型的,即不可变,你没有办法在内部改变传参; ++ 写在主构造器中的代码块会在类初始化的时候被执行,功能类似于 Java 的静态代码块 `static{}` + +```scala +class Person(val name: String, val age: Int) { + + println("功能类似于 Java 的静态代码块 static{}") + + def getDetail: String = { + //name="heibai" 无法通过编译 + name + ":" + age + } +} + +object Person { + def main(args: Array[String]): Unit = { + val person = new Person("heibaiying", 20) + println(person.getDetail) + } +} + +输出: +功能类似于 Java 的静态代码块 static{} +heibaiying:20 +``` + + + +### 2.5 辅助构造器 + +辅助构造器有两点硬性要求: + ++ 辅助构造器的名称必须为 this; ++ 每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始。 + +```scala +class Person(val name: String, val age: Int) { + + private var birthday = "" + + // 1.辅助构造器的名称必须为 this + def this(name: String, age: Int, birthday: String) { + // 2.每个辅助构造器必须以主构造器或其他的辅助构造器的调用开始 + this(name, age) + this.birthday = birthday + } + + // 3.重写 toString 方法 + override def toString: String = name + ":" + age + ":" + birthday +} + +object Person { + def main(args: Array[String]): Unit = { + println(new Person("heibaiying", 20, "2019-02-21")) + } +} +``` + + + +### 2.6 方法传参不可变 + +在 Scala 中,方法传参默认是 val 类型,即不可变,这意味着你在方法体内部不能改变传入的参数。这和 Scala 的设计理念有关,Scala 遵循函数式编程理念,强调方法不应该有副作用。 + +```scala +class Person() { + + def low(word: String): String = { + word="word" // 编译无法通过 + word.toLowerCase + } +} +``` + +
+ +## 三、对象 + +Scala 中的 object(对象) 主要有以下几个作用: + ++ 因为 object 中的变量和方法都是静态的,所以可以用于存放工具类; ++ 可以作为单例对象的容器; ++ 可以作为类的伴生对象; ++ 可以拓展类或特质; ++ 可以拓展 Enumeration 来实现枚举。 + +### 3.1 工具类&单例&全局静态常量&拓展特质 + +这里我们创建一个对象 `Utils`,代码如下: + +```scala +object Utils { + + /* + *1. 相当于 Java 中的静态代码块 static,会在对象初始化时候被执行 + * 这种方式实现的单例模式是饿汉式单例,即无论你的单例对象是否被用到, + * 都在一开始被初始化完成 + */ + val person = new Person + + // 2. 全局固定常量 等价于 Java 的 public static final + val CONSTANT = "固定常量" + + // 3. 全局静态方法 + def low(word: String): String = { + word.toLowerCase + } +} +``` + +其中 Person 类代码如下: + +```scala +class Person() { + println("Person 默认构造器被调用") +} +``` + +新建测试类: + +```scala +// 1.ScalaApp 对象扩展自 trait App +object ScalaApp extends App { + + // 2.验证单例 + println(Utils.person == Utils.person) + + // 3.获取全局常量 + println(Utils.CONSTANT) + + // 4.调用工具类 + println(Utils.low("ABCDEFG")) + +} + +// 输出如下: +Person 默认构造器被调用 +true +固定常量 +abcdefg +``` + +### 3.2 伴生对象 + +在 Java 中,你通常会用到既有实例方法又有静态方法的类,在 Scala 中,可以通过类和与类同名的伴生对象来实现。类和伴生对象必须存在与同一个文件中。 + +```scala +class Person() { + + private val name = "HEIBAIYING" + + def getName: String = { + // 调用伴生对象的方法和属性 + Person.toLow(Person.PREFIX + name) + } +} + +// 伴生对象 +object Person { + + val PREFIX = "prefix-" + + def toLow(word: String): String = { + word.toLowerCase + } + + def main(args: Array[String]): Unit = { + val person = new Person + // 输出 prefix-heibaiying + println(person.getName) + } + +} +``` + + + +### 3.3 实现枚举类 + +Scala 中没有直接提供枚举类,需要通过扩展 `Enumeration`,并调用其中的 Value 方法对所有枚举值进行初始化来实现。 + +```scala +object Color extends Enumeration { + + // 1.类型别名,建议声明,在 import 时有用 + type Color = Value + + // 2.调用 Value 方法 + val GREEN = Value + // 3.只传入 id + val RED = Value(3) + // 4.只传入值 + val BULE = Value("blue") + // 5.传入 id 和值 + val YELLOW = Value(5, "yellow") + // 6. 不传入 id 时,id 为上一个声明变量的 id+1,值默认和变量名相同 + val PINK = Value + +} +``` + +使用枚举类: + +```scala +// 1.使用类型别名导入枚举类 +import com.heibaiying.Color.Color + +object ScalaApp extends App { + + // 2.使用枚举类型,这种情况下需要导入枚举类 + def printColor(color: Color): Unit = { + println(color.toString) + } + + // 3.判断传入值和枚举值是否相等 + println(Color.YELLOW.toString == "yellow") + // 4.遍历枚举类和值 + for (c <- Color.values) println(c.id + ":" + c.toString) +} + +//输出 +true +0:GREEN +3:RED +4:blue +5:yellow +6:PINK +``` + +
+ +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\236\213\345\217\202\346\225\260.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\236\213\345\217\202\346\225\260.md" new file mode 100644 index 0000000..2776d7b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\261\273\345\236\213\345\217\202\346\225\260.md" @@ -0,0 +1,467 @@ +# 类型参数 + + + + +## 一、泛型 + +Scala 支持类型参数化,使得我们能够编写泛型程序。 + +### 1.1 泛型类 + +Java 中使用 `<>` 符号来包含定义的类型参数,Scala 则使用 `[]`。 + +```scala +class Pair[T, S](val first: T, val second: S) { + override def toString: String = first + ":" + second +} +``` + +```scala +object ScalaApp extends App { + + // 使用时候你直接指定参数类型,也可以不指定,由程序自动推断 + val pair01 = new Pair("heibai01", 22) + val pair02 = new Pair[String,Int]("heibai02", 33) + + println(pair01) + println(pair02) +} +``` + +### 1.2 泛型方法 + +函数和方法也支持类型参数。 + +```scala +object Utils { + def getHalf[T](a: Array[T]): Int = a.length / 2 +} +``` + +## 二、类型限定 + +### 2.1 类型上界限定 + +Scala 和 Java 一样,对于对象之间进行大小比较,要求被比较的对象实现 `java.lang.Comparable` 接口。所以如果想对泛型进行比较,需要限定类型上界为 `java.lang.Comparable`,语法为 ` S <: T`,代表类型 S 是类型 T 的子类或其本身。示例如下: + +```scala +// 使用 <: 符号,限定 T 必须是 Comparable[T]的子类型 +class Pair[T <: Comparable[T]](val first: T, val second: T) { + // 返回较小的值 + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +```scala +// 测试代码 +val pair = new Pair("abc", "abcd") +println(pair.smaller) // 输出 abc +``` + +>扩展:如果你想要在 Java 中实现类型变量限定,需要使用关键字 extends 来实现,等价的 Java 代码如下: +> +>```java +>public class Pair> { +> private T first; +> private T second; +> Pair(T first, T second) { +> this.first = first; +> this.second = second; +> } +> public T smaller() { +> return first.compareTo(second) < 0 ? first : second; +> } +>} +>``` + +### 2.2 视图界定 + +在上面的例子中,如果你使用 Int 类型或者 Double 等类型进行测试,点击运行后,你会发现程序根本无法通过编译: + +```scala +val pair1 = new Pair(10, 12) +val pair2 = new Pair(10.0, 12.0) +``` + +之所以出现这样的问题,是因为 Scala 中的 Int 类并没有实现 Comparable 接口。在 Scala 中直接继承 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下: + +```scala +// 除了 compareTo 方法外,还提供了额外的关系符方法 +trait Ordered[A] extends Any with java.lang.Comparable[A] { + def compare(that: A): Int + def < (that: A): Boolean = (this compare that) < 0 + def > (that: A): Boolean = (this compare that) > 0 + def <= (that: A): Boolean = (this compare that) <= 0 + def >= (that: A): Boolean = (this compare that) >= 0 + def compareTo(that: A): Int = compare(that) +} +``` + +之所以在日常的编程中之所以你能够执行 `3>2` 这样的判断操作,是因为程序执行了定义在 `Predef` 中的隐式转换方法 `intWrapper(x: Int) `,将 Int 类型转换为 RichInt 类型,而 RichInt 间接混入了 Ordered 特质,所以能够进行比较。 + +```scala +// Predef.scala +@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) +``` + +
+ +要想解决传入数值无法进行比较的问题,可以使用视图界定。语法为 `T <% U`,代表 T 能够通过隐式转换转为 U,即允许 Int 型参数在无法进行比较的时候转换为 RichInt 类型。示例如下: + +```scala +// 视图界定符号 <% +class Pair[T <% Comparable[T]](val first: T, val second: T) { + // 返回较小的值 + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +> 注:由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以如下的视图界定和上面是等效的: +> +> ```scala +> // 隐式转换为 Ordered[T] +> class Pair[T <% Ordered[T]](val first: T, val second: T) { +> def smaller: T = if (first.compareTo(second) < 0) first else second +> } +> ``` + +### 2.3 类型约束 + +如果你用的 Scala 是 2.11+,会发现视图界定已被标识为废弃。官方推荐使用类型约束 (type constraint) 来实现同样的功能,其本质是使用隐式参数进行隐式转换,示例如下: + +```scala + // 1.使用隐式参数隐式转换为 Comparable[T] +class Pair[T](val first: T, val second: T)(implicit ev: T => Comparable[T]) + def smaller: T = if (first.compareTo(second) < 0) first else second +} + +// 2.由于直接继承 Java 中 Comparable 接口的是特质 Ordered,所以也可以隐式转换为 Ordered[T] +class Pair[T](val first: T, val second: T)(implicit ev: T => Ordered[T]) { + def smaller: T = if (first.compareTo(second) < 0) first else second +} +``` + +当然,隐式参数转换也可以运用在具体的方法上: + +```scala +object PairUtils{ + def smaller[T](a: T, b: T)(implicit order: T => Ordered[T]) = if (a < b) a else b +} +``` + +### 2.4 上下文界定 + +上下文界定的形式为 `T:M`,其中 M 是一个泛型,它要求必须存在一个类型为 M[T]的隐式值,当你声明一个带隐式参数的方法时,需要定义一个隐式默认值。所以上面的程序也可以使用上下文界定进行改写: + +```scala +class Pair[T](val first: T, val second: T) { + // 请注意 这个地方用的是 Ordering[T],而上面视图界定和类型约束,用的是 Ordered[T],两者的区别会在后文给出解释 + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + +// 测试 +val pair= new Pair(88, 66) +println(pair.smaller) //输出:66 +``` + +在上面的示例中,我们无需手动添加隐式默认值就可以完成转换,这是因为 Scala 自动引入了 Ordering[Int]这个隐式值。为了更好的说明上下文界定,下面给出一个自定义类型的比较示例: + +```scala +// 1.定义一个人员类 +class Person(val name: String, val age: Int) { + override def toString: String = name + ":" + age +} + +// 2.继承 Ordering[T],实现自定义比较器,按照自己的规则重写比较方法 +class PersonOrdering extends Ordering[Person] { + override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 +} + +class Pair[T](val first: T, val second: T) { + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + + +object ScalaApp extends App { + + val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) + // 3.定义隐式默认值,如果不定义,则下一行代码无法通过编译 + implicit val ImpPersonOrdering = new PersonOrdering + println(pair.smaller) //输出: bai:66 +} +``` + +### 2.5 ClassTag上下文界定 + +这里先看一个例子:下面这段代码,没有任何语法错误,但是在运行时会抛出异常:`Error: cannot find class tag for element type T`, 这是由于 Scala 和 Java 一样,都存在类型擦除,即**泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉**。对于下面的代码,在运行阶段创建 Array 时,你必须明确指明其类型,但是此时泛型信息已经被擦除,导致出现找不到类型的异常。 + +```scala +object ScalaApp extends App { + def makePair[T](first: T, second: T) = { + // 创建以一个数组 并赋值 + val r = new Array[T](2); r(0) = first; r(1) = second; r + } +} +``` + +Scala 针对这个问题,提供了 ClassTag 上下文界定,即把泛型的信息存储在 ClassTag 中,这样在运行阶段需要时,只需要从 ClassTag 中进行获取即可。其语法为 `T : ClassTag`,示例如下: + +```scala +import scala.reflect._ +object ScalaApp extends App { + def makePair[T : ClassTag](first: T, second: T) = { + val r = new Array[T](2); r(0) = first; r(1) = second; r + } +} +``` + +### 2.6 类型下界限定 + +2.1 小节介绍了类型上界的限定,Scala 同时也支持下界的限定,语法为:`U >: T`,即 U 必须是类型 T 的超类或本身。 + +```scala +// 首席执行官 +class CEO + +// 部门经理 +class Manager extends CEO + +// 本公司普通员工 +class Employee extends Manager + +// 其他公司人员 +class OtherCompany + +object ScalaApp extends App { + + // 限定:只有本公司部门经理以上人员才能获取权限 + def Check[T >: Manager](t: T): T = { + println("获得审核权限") + t + } + + // 错误写法: 省略泛型参数后,以下所有人都能获得权限,显然这是不正确的 + Check(new CEO) + Check(new Manager) + Check(new Employee) + Check(new OtherCompany) + + + // 正确写法,传入泛型参数 + Check[CEO](new CEO) + Check[Manager](new Manager) + /* + * 以下两条语句无法通过编译,异常信息为: + * do not conform to method Check's type parameter bounds(不符合方法 Check 的类型参数边界) + * 这种情况就完成了下界限制,即只有本公司经理及以上的人员才能获得审核权限 + */ + Check[Employee](new Employee) + Check[OtherCompany](new OtherCompany) +} +``` + +### 2.7 多重界定 + ++ 类型变量可以同时有上界和下界。 写法为 :`T > : Lower <: Upper`; + ++ 不能同时有多个上界或多个下界 。但可以要求一个类型实现多个特质,写法为 : + + `T < : Comparable[T] with Serializable with Cloneable`; + ++ 你可以有多个上下文界定,写法为 `T : Ordering : ClassTag` 。 + + + +## 三、Ordering & Ordered + +上文中使用到 Ordering 和 Ordered 特质,它们最主要的区别在于分别继承自不同的 Java 接口:Comparable 和 Comparator: + ++ **Comparable**:可以理解为内置的比较器,实现此接口的对象可以与自身进行比较; ++ **Comparator**:可以理解为外置的比较器;当对象自身并没有定义比较规则的时候,可以传入外部比较器进行比较。 + +为什么 Java 中要同时给出这两个比较接口,这是因为你要比较的对象不一定实现了 Comparable 接口,而你又想对其进行比较,这时候当然你可以修改代码实现 Comparable,但是如果这个类你无法修改 (如源码中的类),这时候就可以使用外置的比较器。同样的问题在 Scala 中当然也会出现,所以 Scala 分别使用了 Ordering 和 Ordered 来继承它们。 + +
+ + + +下面分别给出 Java 中 Comparable 和 Comparator 接口的使用示例: + +### 3.1 Comparable + +```java +import java.util.Arrays; +// 实现 Comparable 接口 +public class Person implements Comparable { + + private String name; + private int age; + + Person(String name,int age) {this.name=name;this.age=age;} + @Override + public String toString() { return name+":"+age; } + + // 核心的方法是重写比较规则,按照年龄进行排序 + @Override + public int compareTo(Person person) { + return this.age - person.age; + } + + public static void main(String[] args) { + Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; + Arrays.sort(peoples); + Arrays.stream(peoples).forEach(System.out::println); + } +} + +输出: +bai:55 +hei:66 +ying:77 +``` + +### 3.2 Comparator + +```java +import java.util.Arrays; +import java.util.Comparator; + +public class Person { + + private String name; + private int age; + + Person(String name,int age) {this.name=name;this.age=age;} + @Override + public String toString() { return name+":"+age; } + + public static void main(String[] args) { + Person[] peoples= {new Person("hei", 66), new Person("bai", 55), new Person("ying", 77)}; + // 这里为了直观直接使用匿名内部类,实现 Comparator 接口 + //如果是 Java8 你也可以写成 Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); + Arrays.sort(peoples, new Comparator() { + @Override + public int compare(Person o1, Person o2) { + return o1.age-o2.age; + } + }); + Arrays.stream(peoples).forEach(System.out::println); + } +} +``` + +使用外置比较器还有一个好处,就是你可以随时定义其排序规则: + +```scala +// 按照年龄大小排序 +Arrays.sort(peoples, Comparator.comparingInt(o -> o.age)); +Arrays.stream(peoples).forEach(System.out::println); +// 按照名字长度倒序排列 +Arrays.sort(peoples, Comparator.comparingInt(o -> -o.name.length())); +Arrays.stream(peoples).forEach(System.out::println); +``` + +### 3.3 上下文界定的优点 + +这里再次给出上下文界定中的示例代码作为回顾: + +```scala +// 1.定义一个人员类 +class Person(val name: String, val age: Int) { + override def toString: String = name + ":" + age +} + +// 2.继承 Ordering[T],实现自定义比较器,这个比较器就是一个外置比较器 +class PersonOrdering extends Ordering[Person] { + override def compare(x: Person, y: Person): Int = if (x.age > y.age) 1 else -1 +} + +class Pair[T](val first: T, val second: T) { + def smaller(implicit ord: Ordering[T]): T = if (ord.compare(first, second) < 0) first else second +} + + +object ScalaApp extends App { + + val pair = new Pair(new Person("hei", 88), new Person("bai", 66)) + // 3.在当前上下文定义隐式默认值,这就相当于传入了外置比较器 + implicit val ImpPersonOrdering = new PersonOrdering + println(pair.smaller) //输出: bai:66 +} +``` + +使用上下文界定和 Ordering 带来的好处是:传入 `Pair` 中的参数不一定需要可比较,只要在比较时传入外置比较器即可。 + +需要注意的是由于隐式默认值二义性的限制,你不能像上面 Java 代码一样,在同一个上下文作用域中传入两个外置比较器,即下面的代码是无法通过编译的。但是你可以在不同的上下文作用域中引入不同的隐式默认值,即使用不同的外置比较器。 + +```scala +implicit val ImpPersonOrdering = new PersonOrdering +println(pair.smaller) +implicit val ImpPersonOrdering2 = new PersonOrdering +println(pair.smaller) +``` + + + +## 四、通配符 + +在实际编码中,通常需要把泛型限定在某个范围内,比如限定为某个类及其子类。因此 Scala 和 Java 一样引入了通配符这个概念,用于限定泛型的范围。不同的是 Java 使用 `?` 表示通配符,Scala 使用 `_` 表示通配符。 + +```scala +class Ceo(val name: String) { + override def toString: String = name +} + +class Manager(name: String) extends Ceo(name) + +class Employee(name: String) extends Manager(name) + +class Pair[T](val first: T, val second: T) { + override def toString: String = "first:" + first + ", second: " + second +} + +object ScalaApp extends App { + // 限定部门经理及以下的人才可以组队 + def makePair(p: Pair[_ <: Manager]): Unit = {println(p)} + makePair(new Pair(new Employee("heibai"), new Manager("ying"))) +} +``` + +目前 Scala 中的通配符在某些复杂情况下还不完善,如下面的语句在 Scala 2.12 中并不能通过编译: + +```scala +def min[T <: Comparable[_ >: T]](p: Pair[T]) ={} +``` + +可以使用以下语法代替: + +```scala +type SuperComparable[T] = Comparable[_ >: T] +def min[T <: SuperComparable[T]](p: Pair[T]) = {} +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\273\247\346\211\277\345\222\214\347\211\271\350\264\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\273\247\346\211\277\345\222\214\347\211\271\350\264\250.md" new file mode 100644 index 0000000..bcb8b24 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\347\273\247\346\211\277\345\222\214\347\211\271\350\264\250.md" @@ -0,0 +1,418 @@ +# 继承和特质 + + + + +## 一、继承 + +### 1.1 Scala中的继承结构 + +Scala 中继承关系如下图: + ++ Any 是整个继承关系的根节点; ++ AnyRef 包含 Scala Classes 和 Java Classes,等价于 Java 中的 java.lang.Object; ++ AnyVal 是所有值类型的一个标记; ++ Null 是所有引用类型的子类型,唯一实例是 null,可以将 null 赋值给除了值类型外的所有类型的变量; ++ Nothing 是所有类型的子类型。 + +
+ +### 1.2 extends & override + +Scala 的集成机制和 Java 有很多相似之处,比如都使用 `extends` 关键字表示继承,都使用 `override` 关键字表示重写父类的方法或成员变量。示例如下: + +```scala +//父类 +class Person { + + var name = "" + // 1.不加任何修饰词,默认为 public,能被子类和外部访问 + var age = 0 + // 2.使用 protected 修饰的变量能子类访问,但是不能被外部访问 + protected var birthday = "" + // 3.使用 private 修饰的变量不能被子类和外部访问 + private var sex = "" + + def setSex(sex: String): Unit = { + this.sex = sex + } + // 4.重写父类的方法建议使用 override 关键字修饰 + override def toString: String = name + ":" + age + ":" + birthday + ":" + sex + +} +``` + +使用 `extends` 关键字实现继承: + +```scala +// 1.使用 extends 关键字实现继承 +class Employee extends Person { + + override def toString: String = "Employee~" + super.toString + + // 2.使用 public 或 protected 关键字修饰的变量能被子类访问 + def setBirthday(date: String): Unit = { + birthday = date + } + +} +``` + +测试继承: + +```scala + +object ScalaApp extends App { + + val employee = new Employee + + employee.name = "heibaiying" + employee.age = 20 + employee.setBirthday("2019-03-05") + employee.setSex("男") + + println(employee) +} + +// 输出: Employee~heibaiying:20:2019-03-05:男 +``` + +### 1.3 调用超类构造器 + +在 Scala 的类中,每个辅助构造器都必须首先调用其他构造器或主构造器,这样就导致了子类的辅助构造器永远无法直接调用超类的构造器,只有主构造器才能调用超类的构造器。所以想要调用超类的构造器,代码示例如下: + +```scala +class Employee(name:String,age:Int,salary:Double) extends Person(name:String,age:Int) { + ..... +} +``` + +### 1.4 类型检查和转换 + +想要实现类检查可以使用 `isInstanceOf`,判断一个实例是否来源于某个类或者其子类,如果是,则可以使用 `asInstanceOf` 进行强制类型转换。 + +```scala +object ScalaApp extends App { + + val employee = new Employee + val person = new Person + + // 1. 判断一个实例是否来源于某个类或者其子类 输出 true + println(employee.isInstanceOf[Person]) + println(person.isInstanceOf[Person]) + + // 2. 强制类型转换 + var p: Person = employee.asInstanceOf[Person] + + // 3. 判断一个实例是否来源于某个类 (而不是其子类) + println(employee.getClass == classOf[Employee]) + +} +``` + +### 1.5 构造顺序和提前定义 + +#### **1. 构造顺序** + +在 Scala 中还有一个需要注意的问题,如果你在子类中重写父类的 val 变量,并且超类的构造器中使用了该变量,那么可能会产生不可预期的错误。下面给出一个示例: + +```scala +// 父类 +class Person { + println("父类的默认构造器") + val range: Int = 10 + val array: Array[Int] = new Array[Int](range) +} + +//子类 +class Employee extends Person { + println("子类的默认构造器") + override val range = 2 +} + +//测试 +object ScalaApp extends App { + val employee = new Employee + println(employee.array.mkString("(", ",", ")")) + +} +``` + +这里初始化 array 用到了变量 range,这里你会发现实际上 array 既不会被初始化 Array(10),也不会被初始化为 Array(2),实际的输出应该如下: + +```properties +父类的默认构造器 +子类的默认构造器 +() +``` + +可以看到 array 被初始化为 Array(0),主要原因在于父类构造器的执行顺序先于子类构造器,这里给出实际的执行步骤: + +1. 父类的构造器被调用,执行 `new Array[Int](range)` 语句; +2. 这里想要得到 range 的值,会去调用子类 range() 方法,因为 `override val` 重写变量的同时也重写了其 get 方法; +3. 调用子类的 range() 方法,自然也是返回子类的 range 值,但是由于子类的构造器还没有执行,这也就意味着对 range 赋值的 `range = 2` 语句还没有被执行,所以自然返回 range 的默认值,也就是 0。 + +这里可能比较疑惑的是为什么 `val range = 2` 没有被执行,却能使用 range 变量,这里因为在虚拟机层面,是先对成员变量先分配存储空间并赋给默认值,之后才赋予给定的值。想要证明这一点其实也比较简单,代码如下: + +```scala +class Person { + // val range: Int = 10 正常代码 array 为 Array(10) + val array: Array[Int] = new Array[Int](range) + val range: Int = 10 //如果把变量的声明放在使用之后,此时数据 array 为 array(0) +} + +object Person { + def main(args: Array[String]): Unit = { + val person = new Person + println(person.array.mkString("(", ",", ")")) + } +} +``` + +#### **2. 提前定义** + +想要解决上面的问题,有以下几种方法: + +(1) . 将变量用 final 修饰,代表不允许被子类重写,即 `final val range: Int = 10 `; + +(2) . 将变量使用 lazy 修饰,代表懒加载,即只有当你实际使用到 array 时候,才去进行初始化; + +```scala +lazy val array: Array[Int] = new Array[Int](range) +``` + +(3) . 采用提前定义,代码如下,代表 range 的定义优先于超类构造器。 + +```scala +class Employee extends { + //这里不能定义其他方法 + override val range = 2 +} with Person { + // 定义其他变量或者方法 + def pr(): Unit = {println("Employee")} +} +``` + +但是这种语法也有其限制:你只能在上面代码块中重写已有的变量,而不能定义新的变量和方法,定义新的变量和方法只能写在下面代码块中。 + +>**注意事项**:类的继承和下文特质 (trait) 的继承都存在这个问题,也同样可以通过提前定义来解决。虽然如此,但还是建议合理设计以规避该类问题。 + +
+ +## 二、抽象类 + +Scala 中允许使用 `abstract` 定义抽象类,并且通过 `extends` 关键字继承它。 + +定义抽象类: + +```scala +abstract class Person { + // 1.定义字段 + var name: String + val age: Int + + // 2.定义抽象方法 + def geDetail: String + + // 3. scala 的抽象类允许定义具体方法 + def print(): Unit = { + println("抽象类中的默认方法") + } +} +``` + +继承抽象类: + +```scala +class Employee extends Person { + // 覆盖抽象类中变量 + override var name: String = "employee" + override val age: Int = 12 + + // 覆盖抽象方法 + def geDetail: String = name + ":" + age +} + +``` + +
+ +## 三、特质 + +### 3.1 trait & with + +Scala 中没有 interface 这个关键字,想要实现类似的功能,可以使用特质 (trait)。trait 等价于 Java 8 中的接口,因为 trait 中既能定义抽象方法,也能定义具体方法,这和 Java 8 中的接口是类似的。 + +```scala +// 1.特质使用 trait 关键字修饰 +trait Logger { + + // 2.定义抽象方法 + def log(msg: String) + + // 3.定义具体方法 + def logInfo(msg: String): Unit = { + println("INFO:" + msg) + } +} +``` + +想要使用特质,需要使用 `extends` 关键字,而不是 `implements` 关键字,如果想要添加多个特质,可以使用 `with` 关键字。 + +```scala +// 1.使用 extends 关键字,而不是 implements,如果想要添加多个特质,可以使用 with 关键字 +class ConsoleLogger extends Logger with Serializable with Cloneable { + + // 2. 实现特质中的抽象方法 + def log(msg: String): Unit = { + println("CONSOLE:" + msg) + } +} +``` + +### 3.2 特质中的字段 + +和方法一样,特质中的字段可以是抽象的,也可以是具体的: + ++ 如果是抽象字段,则混入特质的类需要重写覆盖该字段; ++ 如果是具体字段,则混入特质的类获得该字段,但是并非是通过继承关系得到,而是在编译时候,简单将该字段加入到子类。 + +```scala +trait Logger { + // 抽象字段 + var LogLevel:String + // 具体字段 + var LogType = "FILE" +} +``` + +覆盖抽象字段: + +```scala +class InfoLogger extends Logger { + // 覆盖抽象字段 + override var LogLevel: String = "INFO" +} +``` + +### 3.3 带有特质的对象 + +Scala 支持在类定义的时混入 ` 父类 trait`,而在类实例化为具体对象的时候指明其实际使用的 ` 子类 trait`。示例如下: + +
+ +trait Logger: + +```scala +// 父类 +trait Logger { + // 定义空方法 日志打印 + def log(msg: String) {} +} +``` + +trait ErrorLogger: + +```scala +// 错误日志打印,继承自 Logger +trait ErrorLogger extends Logger { + // 覆盖空方法 + override def log(msg: String): Unit = { + println("Error:" + msg) + } +} +``` + +trait InfoLogger: + +```scala +// 通知日志打印,继承自 Logger +trait InfoLogger extends Logger { + + // 覆盖空方法 + override def log(msg: String): Unit = { + println("INFO:" + msg) + } +} +``` + +具体的使用类: + +```scala +// 混入 trait Logger +class Person extends Logger { + // 调用定义的抽象方法 + def printDetail(detail: String): Unit = { + log(detail) + } +} +``` + +这里通过 main 方法来测试: + +```scala +object ScalaApp extends App { + + // 使用 with 指明需要具体使用的 trait + val person01 = new Person with InfoLogger + val person02 = new Person with ErrorLogger + val person03 = new Person with InfoLogger with ErrorLogger + person01.log("scala") //输出 INFO:scala + person02.log("scala") //输出 Error:scala + person03.log("scala") //输出 Error:scala + +} +``` + +这里前面两个输出比较明显,因为只指明了一个具体的 `trait`,这里需要说明的是第三个输出,**因为 trait 的调用是由右到左开始生效的**,所以这里打印出 `Error:scala`。 + +### 3.4 特质构造顺序 + +`trait` 有默认的无参构造器,但是不支持有参构造器。一个类混入多个特质后初始化顺序应该如下: + +```scala +// 示例 +class Employee extends Person with InfoLogger with ErrorLogger {...} +``` + +1. 超类首先被构造,即 Person 的构造器首先被执行; +2. 特质的构造器在超类构造器之前,在类构造器之后;特质由左到右被构造;每个特质中,父特质首先被构造; + + Logger 构造器执行(Logger 是 InfoLogger 的父类); + + InfoLogger 构造器执行; + + ErrorLogger 构造器执行; +3. 所有超类和特质构造完毕,子类才会被构造。 + +
+ +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + + + + + + + + + + + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\232\220\345\274\217\350\275\254\346\215\242\345\222\214\351\232\220\345\274\217\345\217\202\346\225\260.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\232\220\345\274\217\350\275\254\346\215\242\345\222\214\351\232\220\345\274\217\345\217\202\346\225\260.md" new file mode 100644 index 0000000..d919b63 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\232\220\345\274\217\350\275\254\346\215\242\345\222\214\351\232\220\345\274\217\345\217\202\346\225\260.md" @@ -0,0 +1,356 @@ +# 隐式转换和隐式参数 + + + + +## 一、隐式转换 + +### 1.1 使用隐式转换 + +隐式转换指的是以 `implicit` 关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能。示例如下: + +```scala +// 普通人 +class Person(val name: String) + +// 雷神 +class Thor(val name: String) { + // 正常情况下只有雷神才能举起雷神之锤 + def hammer(): Unit = { + println(name + "举起雷神之锤") + } +} + +object Thor extends App { + // 定义隐式转换方法 将普通人转换为雷神 通常建议方法名使用 source2Target,即:被转换对象 To 转换对象 + implicit def person2Thor(p: Person): Thor = new Thor(p.name) + // 这样普通人也能举起雷神之锤 + new Person("普通人").hammer() +} + +输出: 普通人举起雷神之锤 +``` + + + +### 1.2 隐式转换规则 + +并不是你使用 `implicit` 转换后,隐式转换就一定会发生,比如上面如果不调用 `hammer()` 方法的时候,普通人就还是普通人。通常程序会在以下情况下尝试执行隐式转换: + ++ 当对象访问一个不存在的成员时,即调用的方法不存在或者访问的成员变量不存在; ++ 当对象调用某个方法,该方法存在,但是方法的声明参数与传入参数不匹配时。 + +而在以下三种情况下编译器不会尝试执行隐式转换: + ++ 如果代码能够在不使用隐式转换的前提下通过编译,则不会使用隐式转换; ++ 编译器不会尝试同时执行多个转换,比如 `convert1(convert2(a))*b`; ++ 转换存在二义性,也不会发生转换。 + +这里首先解释一下二义性,上面的代码进行如下修改,由于两个隐式转换都是生效的,所以就存在了二义性: + +```scala +//两个隐式转换都是有效的 +implicit def person2Thor(p: Person): Thor = new Thor(p.name) +implicit def person2Thor2(p: Person): Thor = new Thor(p.name) +// 此时下面这段语句无法通过编译 +new Person("普通人").hammer() +``` + +其次再解释一下多个转换的问题: + +```scala +class ClassA { + override def toString = "This is Class A" +} + +class ClassB { + override def toString = "This is Class B" + def printB(b: ClassB): Unit = println(b) +} + +class ClassC + +class ClassD + +object ImplicitTest extends App { + implicit def A2B(a: ClassA): ClassB = { + println("A2B") + new ClassB + } + + implicit def C2B(c: ClassC): ClassB = { + println("C2B") + new ClassB + } + + implicit def D2C(d: ClassD): ClassC = { + println("D2C") + new ClassC + } + + // 这行代码无法通过编译,因为要调用到 printB 方法,需要执行两次转换 C2B(D2C(ClassD)) + new ClassD().printB(new ClassA) + + /* + * 下面的这一行代码虽然也进行了两次隐式转换,但是两次的转换对象并不是一个对象,所以它是生效的: + * 转换流程如下: + * 1. ClassC 中并没有 printB 方法,因此隐式转换为 ClassB,然后调用 printB 方法; + * 2. 但是 printB 参数类型为 ClassB,然而传入的参数类型是 ClassA,所以需要将参数 ClassA 转换为 ClassB,这是第二次; + * 即: C2B(ClassC) -> ClassB.printB(ClassA) -> ClassB.printB(A2B(ClassA)) -> ClassB.printB(ClassB) + * 转换过程 1 的对象是 ClassC,而转换过程 2 的转换对象是 ClassA,所以虽然是一行代码两次转换,但是仍然是有效转换 + */ + new ClassC().printB(new ClassA) +} + +// 输出: +C2B +A2B +This is Class B +``` + + + +### 1.3 引入隐式转换 + +隐式转换的可以定义在以下三个地方: + ++ 定义在原类型的伴生对象中; ++ 直接定义在执行代码的上下文作用域中; ++ 统一定义在一个文件中,在使用时候导入。 + +上面我们使用的方法相当于直接定义在执行代码的作用域中,下面分别给出其他两种定义的代码示例: + +**定义在原类型的伴生对象中**: + +```scala +class Person(val name: String) +// 在伴生对象中定义隐式转换函数 +object Person{ + implicit def person2Thor(p: Person): Thor = new Thor(p.name) +} +``` + +```scala +class Thor(val name: String) { + def hammer(): Unit = { + println(name + "举起雷神之锤") + } +} +``` + +```scala +// 使用示例 +object ScalaApp extends App { + new Person("普通人").hammer() +} +``` + +**定义在一个公共的对象中**: + +```scala +object Convert { + implicit def person2Thor(p: Person): Thor = new Thor(p.name) +} +``` + +```scala +// 导入 Convert 下所有的隐式转换函数 +import com.heibaiying.Convert._ + +object ScalaApp extends App { + new Person("普通人").hammer() +} +``` + +> 注:Scala 自身的隐式转换函数大部分定义在 `Predef.scala` 中,你可以打开源文件查看,也可以在 Scala 交互式命令行中采用 `:implicit -v` 查看全部隐式转换函数。 + +
+ +## 二、隐式参数 + +### 2.1 使用隐式参数 + +在定义函数或方法时可以使用标记为 `implicit` 的参数,这种情况下,编译器将会查找默认值,提供给函数调用。 + +```scala +// 定义分隔符类 +class Delimiters(val left: String, val right: String) + +object ScalaApp extends App { + + // 进行格式化输出 + def formatted(context: String)(implicit deli: Delimiters): Unit = { + println(deli.left + context + deli.right) + } + + // 定义一个隐式默认值 使用左右中括号作为分隔符 + implicit val bracket = new Delimiters("(", ")") + formatted("this is context") // 输出: (this is context) +} +``` + +关于隐式参数,有两点需要注意: + +1.我们上面定义 `formatted` 函数的时候使用了柯里化,如果你不使用柯里化表达式,按照通常习惯只有下面两种写法: + +```scala +// 这种写法没有语法错误,但是无法通过编译 +def formatted(implicit context: String, deli: Delimiters): Unit = { + println(deli.left + context + deli.right) +} +// 不存在这种写法,IDEA 直接会直接提示语法错误 +def formatted( context: String, implicit deli: Delimiters): Unit = { + println(deli.left + context + deli.right) +} +``` + +上面第一种写法编译的时候会出现下面所示 `error` 信息,从中也可以看出 `implicit` 是作用于参数列表中每个参数的,这显然不是我们想要到达的效果,所以上面的写法采用了柯里化。 + +``` +not enough arguments for method formatted: +(implicit context: String, implicit deli: com.heibaiying.Delimiters) +``` + +2.第二个问题和隐式函数一样,隐式默认值不能存在二义性,否则无法通过编译,示例如下: + +```scala +implicit val bracket = new Delimiters("(", ")") +implicit val brace = new Delimiters("{", "}") +formatted("this is context") +``` + +上面代码无法通过编译,出现错误提示 `ambiguous implicit values`,即隐式值存在冲突。 + + + +### 2.2 引入隐式参数 + +引入隐式参数和引入隐式转换函数方法是一样的,有以下三种方式: + +- 定义在隐式参数对应类的伴生对象中; +- 直接定义在执行代码的上下文作用域中; +- 统一定义在一个文件中,在使用时候导入。 + +我们上面示例程序相当于直接定义执行代码的上下文作用域中,下面给出其他两种方式的示例: + +**定义在隐式参数对应类的伴生对象中**; + +```scala +class Delimiters(val left: String, val right: String) + +object Delimiters { + implicit val bracket = new Delimiters("(", ")") +} +``` + +```scala +// 此时执行代码的上下文中不用定义 +object ScalaApp extends App { + + def formatted(context: String)(implicit deli: Delimiters): Unit = { + println(deli.left + context + deli.right) + } + formatted("this is context") +} +``` + +**统一定义在一个文件中,在使用时候导入**: + +```scala +object Convert { + implicit val bracket = new Delimiters("(", ")") +} +``` + +```scala +// 在使用的时候导入 +import com.heibaiying.Convert.bracket + +object ScalaApp extends App { + def formatted(context: String)(implicit deli: Delimiters): Unit = { + println(deli.left + context + deli.right) + } + formatted("this is context") // 输出: (this is context) +} +``` + + + +### 2.3 利用隐式参数进行隐式转换 + +```scala +def smaller[T] (a: T, b: T) = if (a < b) a else b +``` + +在 Scala 中如果定义了一个如上所示的比较对象大小的泛型方法,你会发现无法通过编译。对于对象之间进行大小比较,Scala 和 Java 一样,都要求被比较的对象需要实现 java.lang.Comparable 接口。在 Scala 中,直接继承 Java 中 Comparable 接口的是特质 Ordered,它在继承 compareTo 方法的基础上,额外定义了关系符方法,源码如下: + +```scala +trait Ordered[A] extends Any with java.lang.Comparable[A] { + def compare(that: A): Int + def < (that: A): Boolean = (this compare that) < 0 + def > (that: A): Boolean = (this compare that) > 0 + def <= (that: A): Boolean = (this compare that) <= 0 + def >= (that: A): Boolean = (this compare that) >= 0 + def compareTo(that: A): Int = compare(that) +} +``` + +所以要想在泛型中解决这个问题,有两种方法: + +#### 1. 使用视图界定 + +```scala +object Pair extends App { + + // 视图界定 + def smaller[T<% Ordered[T]](a: T, b: T) = if (a < b) a else b + + println(smaller(1,2)) //输出 1 +} +``` + +视图限定限制了 T 可以通过隐式转换 `Ordered[T]`,即对象一定可以进行大小比较。在上面的代码中 `smaller(1,2)` 中参数 `1` 和 `2` 实际上是通过定义在 `Predef` 中的隐式转换方法 `intWrapper` 转换为 `RichInt`。 + +```scala +// Predef.scala +@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x) +``` + +为什么要这么麻烦执行隐式转换,原因是 Scala 中的 Int 类型并不能直接进行比较,因为其没有实现 `Ordered` 特质,真正实现 `Ordered` 特质的是 `RichInt`。 + +
+ + + +#### 2. 利用隐式参数进行隐式转换 + +Scala2.11+ 后,视图界定被标识为废弃,官方推荐使用类型限定来解决上面的问题,本质上就是使用隐式参数进行隐式转换。 + +```scala +object Pair extends App { + + // order 既是一个隐式参数也是一个隐式转换,即如果 a 不存在 < 方法,则转换为 order(a) Ordered[T]) = if (a < b) a else b + + println(smaller(1,2)) //输出 1 +} +``` + + + +## 参考资料 + +1. Martin Odersky . Scala 编程 (第 3 版)[M] . 电子工业出版社 . 2018-1-1 +2. 凯.S.霍斯特曼 . 快学 Scala(第 2 版)[M] . 电子工业出版社 . 2017-7 + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\233\206\345\220\210\347\261\273\345\236\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\233\206\345\220\210\347\261\273\345\236\213.md" new file mode 100644 index 0000000..15313a0 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Scala\351\233\206\345\220\210\347\261\273\345\236\213.md" @@ -0,0 +1,259 @@ +# 集合 + + + +## 一、集合简介 + +Scala 中拥有多种集合类型,主要分为可变的和不可变的集合两大类: + ++ **可变集合**: 可以被修改。即可以更改,添加,删除集合中的元素; + ++ **不可变集合类**:不能被修改。对集合执行更改,添加或删除操作都会返回一个新的集合,而不是修改原来的集合。 + +## 二、集合结构 + +Scala 中的大部分集合类都存在三类变体,分别位于 `scala.collection`, `scala.collection.immutable`, `scala.collection.mutable` 包中。还有部分集合类位于 `scala.collection.generic` 包下。 + +- **scala.collection.immutable** :包是中的集合是不可变的; +- **scala.collection.mutable** :包中的集合是可变的; +- **scala.collection** :包中的集合,既可以是可变的,也可以是不可变的。 + +```scala +val sortSet = scala.collection.SortedSet(1, 2, 3, 4, 5) +val mutableSet = collection.mutable.SortedSet(1, 2, 3, 4, 5) +val immutableSet = collection.immutable.SortedSet(1, 2, 3, 4, 5) +``` + +如果你仅写了 `Set` 而没有加任何前缀也没有进行任何 `import`,则 Scala 默认采用不可变集合类。 + +```scala +scala> Set(1,2,3,4,5) +res0: scala.collection.immutable.Set[Int] = Set(5, 1, 2, 3, 4) +``` + +### 3.1 scala.collection + +scala.collection 包中所有集合如下图: + +
+ +### 3.2 scala.collection.mutable + +scala.collection.mutable 包中所有集合如下图: + +
+ +### 3.2 scala.collection.immutable + +scala.collection.immutable 包中所有集合如下图: + +
+ +## 三、Trait Traversable + +Scala 中所有集合的顶层实现是 `Traversable` 。它唯一的抽象方法是 `foreach`: + +```scala +def foreach[U](f: Elem => U) +``` + +实现 `Traversable` 的集合类只需要实现这个抽象方法,其他方法可以从 `Traversable` 继承。`Traversable` 中的所有可用方法如下: + +| **方法** | **作用** | +| ----------------------------------- | ------------------------------------------------------------ | +| **Abstract Method:** | | +| `xs foreach f` | 为 xs 的每个元素执行函数 f | +| **Addition:** | | +| `xs ++ ys` | 一个包含 xs 和 ys 中所有元素的新的集合。 ys 是一个 Traversable 或 Iterator。 | +| **Maps:** | | +| `xs map f` | 对 xs 中每一个元素应用函数 f,并返回一个新的集合 | +| `xs flatMap f` | 对 xs 中每一个元素应用函数 f,最后将结果合并成一个新的集合 | +| `xs collect f` | 对 xs 中每一个元素调用偏函数 f,并返回一个新的集合 | +| **Conversions:** | | +| `xs.toArray` | 将集合转化为一个 Array | +| `xs.toList` | 将集合转化为一个 List | +| `xs.toIterable` | 将集合转化为一个 Iterable | +| `xs.toSeq` | 将集合转化为一个 Seq | +| `xs.toIndexedSeq` | 将集合转化为一个 IndexedSeq | +| `xs.toStream` | 将集合转化为一个延迟计算的流 | +| `xs.toSet` | 将集合转化为一个 Set | +| `xs.toMap` | 将一个(key, value)对的集合转化为一个 Map。 如果当前集合的元素类型不是(key, value)对形式, 则报静态类型错误。 | +| **Copying:** | | +| `xs copyToBuffer buf` | 拷贝集合中所有元素到缓存 buf | +| `xs copyToArray(arr,s,n)` | 从索引 s 开始,将集合中最多 n 个元素复制到数组 arr。 最后两个参数是可选的。 | +| **Size info:** | | +| `xs.isEmpty` | 判断集合是否为空 | +| `xs.nonEmpty` | 判断集合是否包含元素 | +| `xs.size` | 返回集合中元素的个数 | +| `xs.hasDefiniteSize` | 如果 xs 具有有限大小,则为真。 | +| **Element Retrieval:** | | +| `xs.head` | 返回集合中的第一个元素(如果无序,则随机返回) | +| `xs.headOption` | 以 Option 的方式返回集合中的第一个元素, 如果集合为空则返回 None | +| `xs.last` | 返回集合中的最后一个元素(如果无序,则随机返回) | +| `xs.lastOption` | 以 Option 的方式返回集合中的最后一个元素, 如果集合为空则返回 None | +| `xs find p` | 以 Option 的方式返回满足条件 p 的第一个元素, 如果都不满足则返回 None | +| **Subcollection:** | | +| `xs.tail` | 除了第一个元素之外的其他元素组成的集合 | +| `xs.init` | 除了最后一个元素之外的其他元素组成的集合 | +| `xs slice (from, to)` | 返回给定索引范围之内的元素组成的集合 (包含 from 位置的元素但不包含 to 位置的元素) | +| `xs take n` | 返回 xs 的前 n 个元素组成的集合(如果无序,则返回任意 n 个元素) | +| `xs drop n` | 返回 xs 的后 n 个元素组成的集合(如果无序,则返回任意 n 个元素) | +| `xs takeWhile p` | 从第一个元素开始查找满足条件 p 的元素, 直到遇到一个不满足条件的元素,返回所有遍历到的值。 | +| `xs dropWhile p` | 从第一个元素开始查找满足条件 p 的元素, 直到遇到一个不满足条件的元素,返回所有未遍历到的值。 | +| `xs filter p` | 返回满足条件 p 的所有元素的集合 | +| `xs withFilter p` | 集合的非严格的过滤器。后续对 xs 调用方法 map、flatMap 以及 withFilter 都只用作于满足条件 p 的元素,而忽略其他元素 | +| `xs filterNot p` | 返回不满足条件 p 的所有元素组成的集合 | +| **Subdivisions:** | | +| `xs splitAt n` | 在给定位置拆分集合,返回一个集合对 (xs take n, xs drop n) | +| `xs span p` | 根据给定条件拆分集合,返回一个集合对 (xs takeWhile p, xs dropWhile p)。即遍历元素,直到遇到第一个不符合条件的值则结束遍历,将遍历到的值和未遍历到的值分别放入两个集合返回。 | +| `xs partition p` | 按照筛选条件对元素进行分组 | +| `xs groupBy f` | 根据鉴别器函数 f 将 xs 划分为集合映射 | +| **Element Conditions:** | | +| `xs forall p` | 判断集合中所有的元素是否都满足条件 p | +| `xs exists p` | 判断集合中是否存在一个元素满足条件 p | +| `xs count p` | xs 中满足条件 p 的元素的个数 | +| **Folds:** | | +| `(z /: xs) (op)` | 以 z 为初始值,从左到右对 xs 中的元素执行操作为 op 的归约操作 | +| `(xs :\ z) (op)` | 以 z 为初始值,从右到左对 xs 中的元素执行操作为 op 的归约操作 | +| `xs.foldLeft(z) (op)` | 同 (z /: xs) (op) | +| `xs.foldRight(z) (op)` | 同 (xs :\ z) (op) | +| `xs reduceLeft op` | 从左到右对 xs 中的元素执行操作为 op 的归约操作 | +| `xs reduceRight op` | 从右到左对 xs 中的元素执行操作为 op 的归约操作 | +| **Specific Folds:** | | +| `xs.sum` | 累计求和 | +| `xs.product` | 累计求积 | +| `xs.min` | xs 中的最小值 | +| `xs.max` | xs 中的最大值 | +| **String:** | | +| `xs addString (b, start, sep, end)` | 向 StringBuilder b 中添加一个字符串, 该字符串包含 xs 的所有元素。start、seq 和 end 都是可选的,seq 为分隔符,start 为开始符号,end 为结束符号。 | +| `xs mkString (start, seq, end)` | 将集合转化为一个字符串。start、seq 和 end 都是可选的,seq 为分隔符,start 为开始符号,end 为结束符号。 | +| `xs.stringPrefix` | 返回 xs.toString 字符串开头的集合名称 | +| **Views:** | | +| `xs.view` | 生成 xs 的视图 | +| `xs view (from, to)` | 生成 xs 上指定索引范围内元素的视图 | + + + +下面为部分方法的使用示例: + +```scala +scala> List(1, 2, 3, 4, 5, 6).collect { case i if i % 2 == 0 => i * 10 } +res0: List[Int] = List(20, 40, 60) + +scala> List(1, 2, 3, 4, 5, 6).withFilter(_ % 2 == 0).map(_ * 10) +res1: List[Int] = List(20, 40, 60) + +scala> (10 /: List(1, 2, 3)) (_ + _) +res2: Int = 16 + +scala> List(1, 2, 3, -4, 5) takeWhile (_ > 0) +res3: List[Int] = List(1, 2, 3) + +scala> List(1, 2, 3, -4, 5) span (_ > 0) +res4: (List[Int], List[Int]) = (List(1, 2, 3),List(-4, 5)) + +scala> List(1, 2, 3).mkString("[","-","]") +res5: String = [1-2-3] +``` + + + +## 四、Trait Iterable + +Scala 中所有的集合都直接或者间接实现了 `Iterable` 特质,`Iterable` 拓展自 `Traversable`,并额外定义了部分方法: + +| **方法** | **作用** | +| ---------------------- | ------------------------------------------------------------ | +| **Abstract Method:** | | +| `xs.iterator` | 返回一个迭代器,用于遍历 xs 中的元素, 与 foreach 遍历元素的顺序相同。 | +| **Other Iterators:** | | +| `xs grouped size` | 返回一个固定大小的迭代器 | +| `xs sliding size` | 返回一个固定大小的滑动窗口的迭代器 | +| **Subcollections:** | | +| `xs takeRigtht n` | 返回 xs 中最后 n 个元素组成的集合(如果无序,则返回任意 n 个元素组成的集合) | +| `xs dropRight n` | 返回 xs 中除了最后 n 个元素外的部分 | +| **Zippers:** | | +| `xs zip ys` | 返回 xs 和 ys 的对应位置上的元素对组成的集合 | +| `xs zipAll (ys, x, y)` | 返回 xs 和 ys 的对应位置上的元素对组成的集合。其中较短的序列通过附加元素 x 或 y 来扩展以匹配较长的序列。 | +| `xs.zipWithIndex` | 返回一个由 xs 中元素及其索引所组成的元素对的集合 | +| **Comparison:** | | +| `xs sameElements ys` | 测试 xs 和 ys 是否包含相同顺序的相同元素 | + +所有方法示例如下: + +```scala +scala> List(1, 2, 3).iterator.reduce(_ * _ * 10) +res0: Int = 600 + +scala> List("a","b","c","d","e") grouped 2 foreach println +List(a, b) +List(c, d) +List(e) + +scala> List("a","b","c","d","e") sliding 2 foreach println +List(a, b) +List(b, c) +List(c, d) +List(d, e) + +scala> List("a","b","c","d","e").takeRight(3) +res1: List[String] = List(c, d, e) + +scala> List("a","b","c","d","e").dropRight(3) +res2: List[String] = List(a, b) + +scala> List("a","b","c").zip(List(1,2,3)) +res3: List[(String, Int)] = List((a,1), (b,2), (c,3)) + +scala> List("a","b","c","d").zipAll(List(1,2,3),"",4) +res4: List[(String, Int)] = List((a,1), (b,2), (c,3), (d,4)) + +scala> List("a","b","c").zipAll(List(1,2,3,4),"d","") +res5: List[(String, Any)] = List((a,1), (b,2), (c,3), (d,4)) + +scala> List("a", "b", "c").zipWithIndex +res6: List[(String, Int)] = List((a,0), (b,1), (c,2)) + +scala> List("a", "b") sameElements List("a", "b") +res7: Boolean = true + +scala> List("a", "b") sameElements List("b", "a") +res8: Boolean = false +``` + + + +## 五、修改集合 + +当你想对集合添加或者删除元素,需要根据不同的集合类型选择不同的操作符号: + +| 操作符 | 描述 | 集合类型 | +| ------------------------------------------------------------ | ------------------------------------------------- | --------------------- | +| coll(k)
即 coll.apply(k) | 获取指定位置的元素 | Seq, Map | +| coll :+ elem
elem +: coll | 向集合末尾或者集合头增加元素 | Seq | +| coll + elem
coll + (e1, e2, ...) | 追加元素 | Seq, Map | +| coll - elem
coll - (e1, e2, ...) | 删除元素 | Set, Map, ArrayBuffer | +| coll ++ coll2
coll2 ++: coll | 合并集合 | Iterable | +| coll -- coll2 | 移除 coll 中包含的 coll2 中的元素 | Set, Map, ArrayBuffer | +| elem :: lst
lst2 :: lst | 把指定列表 (lst2) 或者元素 (elem) 添加到列表 (lst) 头部 | List | +| list ::: list2 | 合并 List | List | +| set \| set2
set & set2
set &~ set2 | 并集、交集、差集 | Set | +| coll += elem
coll += (e1, e2, ...)
coll ++= coll2
coll -= elem
coll -= (e1, e2, ...)
coll --= coll2 | 添加或者删除元素,并将修改后的结果赋值给集合本身 | 可变集合 | +| elem +=: coll
coll2 ++=: coll | 在集合头部追加元素或集合 | ArrayBuffer | + + + +## 参考资料 + +1. https://docs.scala-lang.org/overviews/collections/overview.html +2. https://docs.scala-lang.org/overviews/collections/trait-traversable.html +3. https://docs.scala-lang.org/overviews/collections/trait-iterable.html diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL_Dataset\345\222\214DataFrame\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL_Dataset\345\222\214DataFrame\347\256\200\344\273\213.md" new file mode 100644 index 0000000..ebb1954 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL_Dataset\345\222\214DataFrame\347\256\200\344\273\213.md" @@ -0,0 +1,147 @@ +# DataFrame和Dataset简介 + + + +## 一、Spark SQL简介 + +Spark SQL 是 Spark 中的一个子模块,主要用于操作结构化数据。它具有以下特点: + ++ 能够将 SQL 查询与 Spark 程序无缝混合,允许您使用 SQL 或 DataFrame API 对结构化数据进行查询; ++ 支持多种开发语言; ++ 支持多达上百种的外部数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC 等; ++ 支持 HiveQL 语法以及 Hive SerDes 和 UDF,允许你访问现有的 Hive 仓库; ++ 支持标准的 JDBC 和 ODBC 连接; ++ 支持优化器,列式存储和代码生成等特性; ++ 支持扩展并能保证容错。 + +
+ +## 二、DataFrame & DataSet + +### 2.1 DataFrame + +为了支持结构化数据的处理,Spark SQL 提供了新的数据结构 DataFrame。DataFrame 是一个由具名列组成的数据集。它在概念上等同于关系数据库中的表或 R/Python 语言中的 `data frame`。 由于 Spark SQL 支持多种语言的开发,所以每种语言都定义了 `DataFrame` 的抽象,主要如下: + +| 语言 | 主要抽象 | +| ------ | -------------------------------------------- | +| Scala | Dataset[T] & DataFrame (Dataset[Row] 的别名) | +| Java | Dataset[T] | +| Python | DataFrame | +| R | DataFrame | + +### 2.2 DataFrame 对比 RDDs + +DataFrame 和 RDDs 最主要的区别在于一个面向的是结构化数据,一个面向的是非结构化数据,它们内部的数据结构如下: + +
+ +DataFrame 内部的有明确 Scheme 结构,即列名、列字段类型都是已知的,这带来的好处是可以减少数据读取以及更好地优化执行计划,从而保证查询效率。 + +**DataFrame 和 RDDs 应该如何选择?** + ++ 如果你想使用函数式编程而不是 DataFrame API,则使用 RDDs; ++ 如果你的数据是非结构化的 (比如流媒体或者字符流),则使用 RDDs, ++ 如果你的数据是结构化的 (如 RDBMS 中的数据) 或者半结构化的 (如日志),出于性能上的考虑,应优先使用 DataFrame。 + +### 2.3 DataSet + +Dataset 也是分布式的数据集合,在 Spark 1.6 版本被引入,它集成了 RDD 和 DataFrame 的优点,具备强类型的特点,同时支持 Lambda 函数,但只能在 Scala 和 Java 语言中使用。在 Spark 2.0 后,为了方便开发者,Spark 将 DataFrame 和 Dataset 的 API 融合到一起,提供了结构化的 API(Structured API),即用户可以通过一套标准的 API 就能完成对两者的操作。 + +> 这里注意一下:DataFrame 被标记为 Untyped API,而 DataSet 被标记为 Typed API,后文会对两者做出解释。 + + + +
+ +### 2.4 静态类型与运行时类型安全 + +静态类型 (Static-typing) 与运行时类型安全 (runtime type-safety) 主要表现如下: + +在实际使用中,如果你用的是 Spark SQL 的查询语句,则直到运行时你才会发现有语法错误,而如果你用的是 DataFrame 和 Dataset,则在编译时就可以发现错误 (这节省了开发时间和整体代价)。DataFrame 和 Dataset 主要区别在于: + +在 DataFrame 中,当你调用了 API 之外的函数,编译器就会报错,但如果你使用了一个不存在的字段名字,编译器依然无法发现。而 Dataset 的 API 都是用 Lambda 函数和 JVM 类型对象表示的,所有不匹配的类型参数在编译时就会被发现。 + +以上这些最终都被解释成关于类型安全图谱,对应开发中的语法和分析错误。在图谱中,Dataset 最严格,但对于开发者来说效率最高。 + +
+ +上面的描述可能并没有那么直观,下面的给出一个 IDEA 中代码编译的示例: + +
+ +这里一个可能的疑惑是 DataFrame 明明是有确定的 Scheme 结构 (即列名、列字段类型都是已知的),但是为什么还是无法对列名进行推断和错误判断,这是因为 DataFrame 是 Untyped 的。 + +### 2.5 Untyped & Typed + +在上面我们介绍过 DataFrame API 被标记为 `Untyped API`,而 DataSet API 被标记为 `Typed API`。DataFrame 的 `Untyped` 是相对于语言或 API 层面而言,它确实有明确的 Scheme 结构,即列名,列类型都是确定的,但这些信息完全由 Spark 来维护,Spark 只会在运行时检查这些类型和指定类型是否一致。这也就是为什么在 Spark 2.0 之后,官方推荐把 DataFrame 看做是 `DatSet[Row]`,Row 是 Spark 中定义的一个 `trait`,其子类中封装了列字段的信息。 + +相对而言,DataSet 是 `Typed` 的,即强类型。如下面代码,DataSet 的类型由 Case Class(Scala) 或者 Java Bean(Java) 来明确指定的,在这里即每一行数据代表一个 `Person`,这些信息由 JVM 来保证正确性,所以字段名错误和类型错误在编译的时候就会被 IDE 所发现。 + +```scala +case class Person(name: String, age: Long) +val dataSet: Dataset[Person] = spark.read.json("people.json").as[Person] +``` + + + +## 三、DataFrame & DataSet & RDDs 总结 + +这里对三者做一下简单的总结: + ++ RDDs 适合非结构化数据的处理,而 DataFrame & DataSet 更适合结构化数据和半结构化的处理; ++ DataFrame & DataSet 可以通过统一的 Structured API 进行访问,而 RDDs 则更适合函数式编程的场景; ++ 相比于 DataFrame 而言,DataSet 是强类型的 (Typed),有着更为严格的静态类型检查; ++ DataSets、DataFrames、SQL 的底层都依赖了 RDDs API,并对外提供结构化的访问接口。 + +
+ + + +## 四、Spark SQL的运行原理 + +DataFrame、DataSet 和 Spark SQL 的实际执行流程都是相同的: + +1. 进行 DataFrame/Dataset/SQL 编程; +2. 如果是有效的代码,即代码没有编译错误,Spark 会将其转换为一个逻辑计划; +3. Spark 将此逻辑计划转换为物理计划,同时进行代码优化; +4. Spark 然后在集群上执行这个物理计划 (基于 RDD 操作) 。 + +### 4.1 逻辑计划(Logical Plan) + +执行的第一个阶段是将用户代码转换成一个逻辑计划。它首先将用户代码转换成 `unresolved logical plan`(未解决的逻辑计划),之所以这个计划是未解决的,是因为尽管您的代码在语法上是正确的,但是它引用的表或列可能不存在。 Spark 使用 `analyzer`(分析器) 基于 `catalog`(存储的所有表和 `DataFrames` 的信息) 进行解析。解析失败则拒绝执行,解析成功则将结果传给 `Catalyst` 优化器 (`Catalyst Optimizer`),优化器是一组规则的集合,用于优化逻辑计划,通过谓词下推等方式进行优化,最终输出优化后的逻辑执行计划。 + +
+ + + +### 4.2 物理计划(Physical Plan) + +得到优化后的逻辑计划后,Spark 就开始了物理计划过程。 它通过生成不同的物理执行策略,并通过成本模型来比较它们,从而选择一个最优的物理计划在集群上面执行的。物理规划的输出结果是一系列的 RDDs 和转换关系 (transformations)。 + +
+ +### 4.3 执行 + +在选择一个物理计划后,Spark 运行其 RDDs 代码,并在运行时执行进一步的优化,生成本地 Java 字节码,最后将运行结果返回给用户。 + + + +## 参考资料 + +1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 +2. [Spark SQL, DataFrames and Datasets Guide](https://spark.apache.org/docs/latest/sql-programming-guide.html) +3. [且谈 Apache Spark 的 API 三剑客:RDD、DataFrame 和 Dataset(译文)](https://www.infoq.cn/article/three-apache-spark-apis-rdds-dataframes-and-datasets) +4. [A Tale of Three Apache Spark APIs: RDDs vs DataFrames and Datasets(原文)](https://databricks.com/blog/2016/07/14/a-tale-of-three-apache-spark-apis-rdds-dataframes-and-datasets.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\244\226\351\203\250\346\225\260\346\215\256\346\272\220.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\244\226\351\203\250\346\225\260\346\215\256\346\272\220.md" new file mode 100644 index 0000000..76ace06 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\244\226\351\203\250\346\225\260\346\215\256\346\272\220.md" @@ -0,0 +1,499 @@ +# Spark SQL 外部数据源 + + + +## 一、简介 + +### 1.1 多数据源支持 + +Spark 支持以下六个核心数据源,同时 Spark 社区还提供了多达上百种数据源的读取方式,能够满足绝大部分使用场景。 + +- CSV +- JSON +- Parquet +- ORC +- JDBC/ODBC connections +- Plain-text files + +> 注:以下所有测试文件均可从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载 + +### 1.2 读数据格式 + +所有读取 API 遵循以下调用格式: + +```scala +// 格式 +DataFrameReader.format(...).option("key", "value").schema(...).load() + +// 示例 +spark.read.format("csv") +.option("mode", "FAILFAST") // 读取模式 +.option("inferSchema", "true") // 是否自动推断 schema +.option("path", "path/to/file(s)") // 文件路径 +.schema(someSchema) // 使用预定义的 schema +.load() +``` + +读取模式有以下三种可选项: + +| 读模式 | 描述 | +| --------------- | ------------------------------------------------------------ | +| `permissive` | 当遇到损坏的记录时,将其所有字段设置为 null,并将所有损坏的记录放在名为 _corruption t_record 的字符串列中 | +| `dropMalformed` | 删除格式不正确的行 | +| `failFast` | 遇到格式不正确的数据时立即失败 | + +### 1.3 写数据格式 + +```scala +// 格式 +DataFrameWriter.format(...).option(...).partitionBy(...).bucketBy(...).sortBy(...).save() + +//示例 +dataframe.write.format("csv") +.option("mode", "OVERWRITE") //写模式 +.option("dateFormat", "yyyy-MM-dd") //日期格式 +.option("path", "path/to/file(s)") +.save() +``` + +写数据模式有以下四种可选项: + +| Scala/Java | 描述 | +| :----------------------- | :----------------------------------------------------------- | +| `SaveMode.ErrorIfExists` | 如果给定的路径已经存在文件,则抛出异常,这是写数据默认的模式 | +| `SaveMode.Append` | 数据以追加的方式写入 | +| `SaveMode.Overwrite` | 数据以覆盖的方式写入 | +| `SaveMode.Ignore` | 如果给定的路径已经存在文件,则不做任何操作 | + +
+ +## 二、CSV + +CSV 是一种常见的文本文件格式,其中每一行表示一条记录,记录中的每个字段用逗号分隔。 + +### 2.1 读取CSV文件 + +自动推断类型读取读取示例: + +```scala +spark.read.format("csv") +.option("header", "false") // 文件中的第一行是否为列的名称 +.option("mode", "FAILFAST") // 是否快速失败 +.option("inferSchema", "true") // 是否自动推断 schema +.load("/usr/file/csv/dept.csv") +.show() +``` + +使用预定义类型: + +```scala +import org.apache.spark.sql.types.{StructField, StructType, StringType,LongType} +//预定义数据格式 +val myManualSchema = new StructType(Array( + StructField("deptno", LongType, nullable = false), + StructField("dname", StringType,nullable = true), + StructField("loc", StringType,nullable = true) +)) +spark.read.format("csv") +.option("mode", "FAILFAST") +.schema(myManualSchema) +.load("/usr/file/csv/dept.csv") +.show() +``` + +### 2.2 写入CSV文件 + +```scala +df.write.format("csv").mode("overwrite").save("/tmp/csv/dept2") +``` + +也可以指定具体的分隔符: + +```scala +df.write.format("csv").mode("overwrite").option("sep", "\t").save("/tmp/csv/dept2") +``` + +### 2.3 可选配置 + +为节省主文篇幅,所有读写配置项见文末 9.1 小节。 + +
+ +## 三、JSON + +### 3.1 读取JSON文件 + +```json +spark.read.format("json").option("mode", "FAILFAST").load("/usr/file/json/dept.json").show(5) +``` + +需要注意的是:默认不支持一条数据记录跨越多行 (如下),可以通过配置 `multiLine` 为 `true` 来进行更改,其默认值为 `false`。 + +```json +// 默认支持单行 +{"DEPTNO": 10,"DNAME": "ACCOUNTING","LOC": "NEW YORK"} + +//默认不支持多行 +{ + "DEPTNO": 10, + "DNAME": "ACCOUNTING", + "LOC": "NEW YORK" +} +``` + +### 3.2 写入JSON文件 + +```scala +df.write.format("json").mode("overwrite").save("/tmp/spark/json/dept") +``` + +### 3.3 可选配置 + +为节省主文篇幅,所有读写配置项见文末 9.2 小节。 + +
+ +## 四、Parquet + + Parquet 是一个开源的面向列的数据存储,它提供了多种存储优化,允许读取单独的列非整个文件,这不仅节省了存储空间而且提升了读取效率,它是 Spark 是默认的文件格式。 + +### 4.1 读取Parquet文件 + +```scala +spark.read.format("parquet").load("/usr/file/parquet/dept.parquet").show(5) +``` + +### 2.2 写入Parquet文件 + +```scala +df.write.format("parquet").mode("overwrite").save("/tmp/spark/parquet/dept") +``` + +### 2.3 可选配置 + +Parquet 文件有着自己的存储规则,因此其可选配置项比较少,常用的有如下两个: + +| 读写操作 | 配置项 | 可选值 | 默认值 | 描述 | +| -------- | -------------------- | ------------------------------------------------------------ | ------------------------------------------- | ------------------------------------------------------------ | +| Write | compression or codec | None,
uncompressed,
bzip2,
deflate, gzip,
lz4, or snappy | None | 压缩文件格式 | +| Read | mergeSchema | true, false | 取决于配置项 `spark.sql.parquet.mergeSchema` | 当为真时,Parquet 数据源将所有数据文件收集的 Schema 合并在一起,否则将从摘要文件中选择 Schema,如果没有可用的摘要文件,则从随机数据文件中选择 Schema。 | + +> 更多可选配置可以参阅官方文档:https://spark.apache.org/docs/latest/sql-data-sources-parquet.html + +
+ +## 五、ORC + +ORC 是一种自描述的、类型感知的列文件格式,它针对大型数据的读写进行了优化,也是大数据中常用的文件格式。 + +### 5.1 读取ORC文件 + +```scala +spark.read.format("orc").load("/usr/file/orc/dept.orc").show(5) +``` + +### 4.2 写入ORC文件 + +```scala +csvFile.write.format("orc").mode("overwrite").save("/tmp/spark/orc/dept") +``` + +
+ +## 六、SQL Databases + +Spark 同样支持与传统的关系型数据库进行数据读写。但是 Spark 程序默认是没有提供数据库驱动的,所以在使用前需要将对应的数据库驱动上传到安装目录下的 `jars` 目录中。下面示例使用的是 Mysql 数据库,使用前需要将对应的 `mysql-connector-java-x.x.x.jar` 上传到 `jars` 目录下。 + +### 6.1 读取数据 + +读取全表数据示例如下,这里的 `help_keyword` 是 mysql 内置的字典表,只有 `help_keyword_id` 和 `name` 两个字段。 + +```scala +spark.read +.format("jdbc") +.option("driver", "com.mysql.jdbc.Driver") //驱动 +.option("url", "jdbc:mysql://127.0.0.1:3306/mysql") //数据库地址 +.option("dbtable", "help_keyword") //表名 +.option("user", "root").option("password","root").load().show(10) +``` + +从查询结果读取数据: + +```scala +val pushDownQuery = """(SELECT * FROM help_keyword WHERE help_keyword_id <20) AS help_keywords""" +spark.read.format("jdbc") +.option("url", "jdbc:mysql://127.0.0.1:3306/mysql") +.option("driver", "com.mysql.jdbc.Driver") +.option("user", "root").option("password", "root") +.option("dbtable", pushDownQuery) +.load().show() + +//输出 ++---------------+-----------+ +|help_keyword_id| name| ++---------------+-----------+ +| 0| <>| +| 1| ACTION| +| 2| ADD| +| 3|AES_DECRYPT| +| 4|AES_ENCRYPT| +| 5| AFTER| +| 6| AGAINST| +| 7| AGGREGATE| +| 8| ALGORITHM| +| 9| ALL| +| 10| ALTER| +| 11| ANALYSE| +| 12| ANALYZE| +| 13| AND| +| 14| ARCHIVE| +| 15| AREA| +| 16| AS| +| 17| ASBINARY| +| 18| ASC| +| 19| ASTEXT| ++---------------+-----------+ +``` + +也可以使用如下的写法进行数据的过滤: + +```scala +val props = new java.util.Properties +props.setProperty("driver", "com.mysql.jdbc.Driver") +props.setProperty("user", "root") +props.setProperty("password", "root") +val predicates = Array("help_keyword_id < 10 OR name = 'WHEN'") //指定数据过滤条件 +spark.read.jdbc("jdbc:mysql://127.0.0.1:3306/mysql", "help_keyword", predicates, props).show() + +//输出: ++---------------+-----------+ +|help_keyword_id| name| ++---------------+-----------+ +| 0| <>| +| 1| ACTION| +| 2| ADD| +| 3|AES_DECRYPT| +| 4|AES_ENCRYPT| +| 5| AFTER| +| 6| AGAINST| +| 7| AGGREGATE| +| 8| ALGORITHM| +| 9| ALL| +| 604| WHEN| ++---------------+-----------+ +``` + +可以使用 `numPartitions` 指定读取数据的并行度: + +```scala +option("numPartitions", 10) +``` + +在这里,除了可以指定分区外,还可以设置上界和下界,任何小于下界的值都会被分配在第一个分区中,任何大于上界的值都会被分配在最后一个分区中。 + +```scala +val colName = "help_keyword_id" //用于判断上下界的列 +val lowerBound = 300L //下界 +val upperBound = 500L //上界 +val numPartitions = 10 //分区综述 +val jdbcDf = spark.read.jdbc("jdbc:mysql://127.0.0.1:3306/mysql","help_keyword", + colName,lowerBound,upperBound,numPartitions,props) +``` + +想要验证分区内容,可以使用 `mapPartitionsWithIndex` 这个算子,代码如下: + +```scala +jdbcDf.rdd.mapPartitionsWithIndex((index, iterator) => { + val buffer = new ListBuffer[String] + while (iterator.hasNext) { + buffer.append(index + "分区:" + iterator.next()) + } + buffer.toIterator +}).foreach(println) +``` + +执行结果如下:`help_keyword` 这张表只有 600 条左右的数据,本来数据应该均匀分布在 10 个分区,但是 0 分区里面却有 319 条数据,这是因为设置了下限,所有小于 300 的数据都会被限制在第一个分区,即 0 分区。同理所有大于 500 的数据被分配在 9 分区,即最后一个分区。 + +
+ +### 6.2 写入数据 + +```scala +val df = spark.read.format("json").load("/usr/file/json/emp.json") +df.write +.format("jdbc") +.option("url", "jdbc:mysql://127.0.0.1:3306/mysql") +.option("user", "root").option("password", "root") +.option("dbtable", "emp") +.save() +``` + +
+ +## 七、Text + +Text 文件在读写性能方面并没有任何优势,且不能表达明确的数据结构,所以其使用的比较少,读写操作如下: + +### 7.1 读取Text数据 + +```scala +spark.read.textFile("/usr/file/txt/dept.txt").show() +``` + +### 7.2 写入Text数据 + +```scala +df.write.text("/tmp/spark/txt/dept") +``` + +
+ +## 八、数据读写高级特性 + +### 8.1 并行读 + +多个 Executors 不能同时读取同一个文件,但它们可以同时读取不同的文件。这意味着当您从一个包含多个文件的文件夹中读取数据时,这些文件中的每一个都将成为 DataFrame 中的一个分区,并由可用的 Executors 并行读取。 + +### 8.2 并行写 + +写入的文件或数据的数量取决于写入数据时 DataFrame 拥有的分区数量。默认情况下,每个数据分区写一个文件。 + +### 8.3 分区写入 + +分区和分桶这两个概念和 Hive 中分区表和分桶表是一致的。都是将数据按照一定规则进行拆分存储。需要注意的是 `partitionBy` 指定的分区和 RDD 中分区不是一个概念:这里的**分区表现为输出目录的子目录**,数据分别存储在对应的子目录中。 + +```scala +val df = spark.read.format("json").load("/usr/file/json/emp.json") +df.write.mode("overwrite").partitionBy("deptno").save("/tmp/spark/partitions") +``` + +输出结果如下:可以看到输出被按照部门编号分为三个子目录,子目录中才是对应的输出文件。 + +
+ +### 8.3 分桶写入 + +分桶写入就是将数据按照指定的列和桶数进行散列,目前分桶写入只支持保存为表,实际上这就是 Hive 的分桶表。 + +```scala +val numberBuckets = 10 +val columnToBucketBy = "empno" +df.write.format("parquet").mode("overwrite") +.bucketBy(numberBuckets, columnToBucketBy).saveAsTable("bucketedFiles") +``` + +### 8.5 文件大小管理 + +如果写入产生小文件数量过多,这时会产生大量的元数据开销。Spark 和 HDFS 一样,都不能很好的处理这个问题,这被称为“small file problem”。同时数据文件也不能过大,否则在查询时会有不必要的性能开销,因此要把文件大小控制在一个合理的范围内。 + +在上文我们已经介绍过可以通过分区数量来控制生成文件的数量,从而间接控制文件大小。Spark 2.2 引入了一种新的方法,以更自动化的方式控制文件大小,这就是 `maxRecordsPerFile` 参数,它允许你通过控制写入文件的记录数来控制文件大小。 + +```scala + // Spark 将确保文件最多包含 5000 条记录 +df.write.option(“maxRecordsPerFile”, 5000) +``` + +
+ +## 九、可选配置附录 + +### 9.1 CSV读写可选配置 + +| 读\写操作 | 配置项 | 可选值 | 默认值 | 描述 | +| --------- | --------------------------- | ------------------------------------------------------------ | -------------------------- | ------------------------------------------------------------ | +| Both | seq | 任意字符 | `,`(逗号) | 分隔符 | +| Both | header | true, false | false | 文件中的第一行是否为列的名称。 | +| Read | escape | 任意字符 | \ | 转义字符 | +| Read | inferSchema | true, false | false | 是否自动推断列类型 | +| Read | ignoreLeadingWhiteSpace | true, false | false | 是否跳过值前面的空格 | +| Both | ignoreTrailingWhiteSpace | true, false | false | 是否跳过值后面的空格 | +| Both | nullValue | 任意字符 | “” | 声明文件中哪个字符表示空值 | +| Both | nanValue | 任意字符 | NaN | 声明哪个值表示 NaN 或者缺省值 | +| Both | positiveInf | 任意字符 | Inf | 正无穷 | +| Both | negativeInf | 任意字符 | -Inf | 负无穷 | +| Both | compression or codec | None,
uncompressed,
bzip2, deflate,
gzip, lz4, or
snappy | none | 文件压缩格式 | +| Both | dateFormat | 任何能转换为 Java 的
SimpleDataFormat 的字符串 | yyyy-MM-dd | 日期格式 | +| Both | timestampFormat | 任何能转换为 Java 的
SimpleDataFormat 的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | 时间戳格式 | +| Read | maxColumns | 任意整数 | 20480 | 声明文件中的最大列数 | +| Read | maxCharsPerColumn | 任意整数 | 1000000 | 声明一个列中的最大字符数。 | +| Read | escapeQuotes | true, false | true | 是否应该转义行中的引号。 | +| Read | maxMalformedLogPerPartition | 任意整数 | 10 | 声明每个分区中最多允许多少条格式错误的数据,超过这个值后格式错误的数据将不会被读取 | +| Write | quoteAll | true, false | false | 指定是否应该将所有值都括在引号中,而不只是转义具有引号字符的值。 | +| Read | multiLine | true, false | false | 是否允许每条完整记录跨域多行 | + +### 9.2 JSON读写可选配置 + +| 读\写操作 | 配置项 | 可选值 | 默认值 | +| --------- | ---------------------------------- | ------------------------------------------------------------ | -------------------------------- | +| Both | compression or codec | None,
uncompressed,
bzip2, deflate,
gzip, lz4, or
snappy | none | +| Both | dateFormat | 任何能转换为 Java 的 SimpleDataFormat 的字符串 | yyyy-MM-dd | +| Both | timestampFormat | 任何能转换为 Java 的 SimpleDataFormat 的字符串 | yyyy-MMdd’T’HH:mm:ss.SSSZZ | +| Read | primitiveAsString | true, false | false | +| Read | allowComments | true, false | false | +| Read | allowUnquotedFieldNames | true, false | false | +| Read | allowSingleQuotes | true, false | true | +| Read | allowNumericLeadingZeros | true, false | false | +| Read | allowBackslashEscapingAnyCharacter | true, false | false | +| Read | columnNameOfCorruptRecord | true, false | Value of spark.sql.column&NameOf | +| Read | multiLine | true, false | false | + +### 9.3 数据库读写可选配置 + +| 属性名称 | 含义 | +| ------------------------------------------ | ------------------------------------------------------------ | +| url | 数据库地址 | +| dbtable | 表名称 | +| driver | 数据库驱动 | +| partitionColumn,
lowerBound, upperBoun | 分区总数,上界,下界 | +| numPartitions | 可用于表读写并行性的最大分区数。如果要写的分区数量超过这个限制,那么可以调用 coalesce(numpartition) 重置分区数。 | +| fetchsize | 每次往返要获取多少行数据。此选项仅适用于读取数据。 | +| batchsize | 每次往返插入多少行数据,这个选项只适用于写入数据。默认值是 1000。 | +| isolationLevel | 事务隔离级别:可以是 NONE,READ_COMMITTED, READ_UNCOMMITTED,REPEATABLE_READ 或 SERIALIZABLE,即标准事务隔离级别。
默认值是 READ_UNCOMMITTED。这个选项只适用于数据读取。 | +| createTableOptions | 写入数据时自定义创建表的相关配置 | +| createTableColumnTypes | 写入数据时自定义创建列的列类型 | + +> 数据库读写更多配置可以参阅官方文档:https://spark.apache.org/docs/latest/sql-data-sources-jdbc.html + + + +## 参考资料 + +1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 +2. https://spark.apache.org/docs/latest/sql-data-sources.html + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\270\270\347\224\250\350\201\232\345\220\210\345\207\275\346\225\260.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\270\270\347\224\250\350\201\232\345\220\210\345\207\275\346\225\260.md" new file mode 100644 index 0000000..0d0ea52 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\345\270\270\347\224\250\350\201\232\345\220\210\345\207\275\346\225\260.md" @@ -0,0 +1,339 @@ +# 聚合函数Aggregations + + + + +## 一、简单聚合 + +### 1.1 数据准备 + +```scala +// 需要导入 spark sql 内置的函数包 +import org.apache.spark.sql.functions._ + +val spark = SparkSession.builder().appName("aggregations").master("local[2]").getOrCreate() +val empDF = spark.read.json("/usr/file/json/emp.json") +// 注册为临时视图,用于后面演示 SQL 查询 +empDF.createOrReplaceTempView("emp") +empDF.show() +``` + +> 注:emp.json 可以从本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下载。 + +### 1.2 count + +```scala +// 计算员工人数 +empDF.select(count("ename")).show() +``` + +### 1.3 countDistinct + +```scala +// 计算姓名不重复的员工人数 +empDF.select(countDistinct("deptno")).show() +``` + +### 1.4 approx_count_distinct + +通常在使用大型数据集时,你可能关注的只是近似值而不是准确值,这时可以使用 approx_count_distinct 函数,并可以使用第二个参数指定最大允许误差。 + +```scala +empDF.select(approx_count_distinct ("ename",0.1)).show() +``` + +### 1.5 first & last + +获取 DataFrame 中指定列的第一个值或者最后一个值。 + +```scala +empDF.select(first("ename"),last("job")).show() +``` + +### 1.6 min & max + +获取 DataFrame 中指定列的最小值或者最大值。 + +```scala +empDF.select(min("sal"),max("sal")).show() +``` + +### 1.7 sum & sumDistinct + +求和以及求指定列所有不相同的值的和。 + +```scala +empDF.select(sum("sal")).show() +empDF.select(sumDistinct("sal")).show() +``` + +### 1.8 avg + +内置的求平均数的函数。 + +```scala +empDF.select(avg("sal")).show() +``` + +### 1.9 数学函数 + +Spark SQL 中还支持多种数学聚合函数,用于通常的数学计算,以下是一些常用的例子: + +```scala +// 1.计算总体方差、均方差、总体标准差、样本标准差 +empDF.select(var_pop("sal"), var_samp("sal"), stddev_pop("sal"), stddev_samp("sal")).show() + +// 2.计算偏度和峰度 +empDF.select(skewness("sal"), kurtosis("sal")).show() + +// 3. 计算两列的皮尔逊相关系数、样本协方差、总体协方差。(这里只是演示,员工编号和薪资两列实际上并没有什么关联关系) +empDF.select(corr("empno", "sal"), covar_samp("empno", "sal"),covar_pop("empno", "sal")).show() +``` + +### 1.10 聚合数据到集合 + +```scala +scala> empDF.agg(collect_set("job"), collect_list("ename")).show() + +输出: ++--------------------+--------------------+ +| collect_set(job)| collect_list(ename)| ++--------------------+--------------------+ +|[MANAGER, SALESMA...|[SMITH, ALLEN, WA...| ++--------------------+--------------------+ +``` + + + +## 二、分组聚合 + +### 2.1 简单分组 + +```scala +empDF.groupBy("deptno", "job").count().show() +//等价 SQL +spark.sql("SELECT deptno, job, count(*) FROM emp GROUP BY deptno, job").show() + +输出: ++------+---------+-----+ +|deptno| job|count| ++------+---------+-----+ +| 10|PRESIDENT| 1| +| 30| CLERK| 1| +| 10| MANAGER| 1| +| 30| MANAGER| 1| +| 20| CLERK| 2| +| 30| SALESMAN| 4| +| 20| ANALYST| 2| +| 10| CLERK| 1| +| 20| MANAGER| 1| ++------+---------+-----+ +``` + +### 2.2 分组聚合 + +```scala +empDF.groupBy("deptno").agg(count("ename").alias("人数"), sum("sal").alias("总工资")).show() +// 等价语法 +empDF.groupBy("deptno").agg("ename"->"count","sal"->"sum").show() +// 等价 SQL +spark.sql("SELECT deptno, count(ename) ,sum(sal) FROM emp GROUP BY deptno").show() + +输出: ++------+----+------+ +|deptno|人数|总工资| ++------+----+------+ +| 10| 3|8750.0| +| 30| 6|9400.0| +| 20| 5|9375.0| ++------+----+------+ +``` + + + +## 三、自定义聚合函数 + +Scala 提供了两种自定义聚合函数的方法,分别如下: + +- 有类型的自定义聚合函数,主要适用于 DataSet; +- 无类型的自定义聚合函数,主要适用于 DataFrame。 + +以下分别使用两种方式来自定义一个求平均值的聚合函数,这里以计算员工平均工资为例。两种自定义方式分别如下: + +### 3.1 有类型的自定义函数 + +```scala +import org.apache.spark.sql.expressions.Aggregator +import org.apache.spark.sql.{Encoder, Encoders, SparkSession, functions} + +// 1.定义员工类,对于可能存在 null 值的字段需要使用 Option 进行包装 +case class Emp(ename: String, comm: scala.Option[Double], deptno: Long, empno: Long, + hiredate: String, job: String, mgr: scala.Option[Long], sal: Double) + +// 2.定义聚合操作的中间输出类型 +case class SumAndCount(var sum: Double, var count: Long) + +/* 3.自定义聚合函数 + * @IN 聚合操作的输入类型 + * @BUF reduction 操作输出值的类型 + * @OUT 聚合操作的输出类型 + */ +object MyAverage extends Aggregator[Emp, SumAndCount, Double] { + + // 4.用于聚合操作的的初始零值 + override def zero: SumAndCount = SumAndCount(0, 0) + + // 5.同一分区中的 reduce 操作 + override def reduce(avg: SumAndCount, emp: Emp): SumAndCount = { + avg.sum += emp.sal + avg.count += 1 + avg + } + + // 6.不同分区中的 merge 操作 + override def merge(avg1: SumAndCount, avg2: SumAndCount): SumAndCount = { + avg1.sum += avg2.sum + avg1.count += avg2.count + avg1 + } + + // 7.定义最终的输出类型 + override def finish(reduction: SumAndCount): Double = reduction.sum / reduction.count + + // 8.中间类型的编码转换 + override def bufferEncoder: Encoder[SumAndCount] = Encoders.product + + // 9.输出类型的编码转换 + override def outputEncoder: Encoder[Double] = Encoders.scalaDouble +} + +object SparkSqlApp { + + // 测试方法 + def main(args: Array[String]): Unit = { + + val spark = SparkSession.builder().appName("Spark-SQL").master("local[2]").getOrCreate() + import spark.implicits._ + val ds = spark.read.json("file/emp.json").as[Emp] + + // 10.使用内置 avg() 函数和自定义函数分别进行计算,验证自定义函数是否正确 + val myAvg = ds.select(MyAverage.toColumn.name("average_sal")).first() + val avg = ds.select(functions.avg(ds.col("sal"))).first().get(0) + + println("自定义 average 函数 : " + myAvg) + println("内置的 average 函数 : " + avg) + } +} +``` + +自定义聚合函数需要实现的方法比较多,这里以绘图的方式来演示其执行流程,以及每个方法的作用: + +
+ + + +关于 `zero`,`reduce`,`merge`,`finish` 方法的作用在上图都有说明,这里解释一下中间类型和输出类型的编码转换,这个写法比较固定,基本上就是两种情况: + +- 自定义类型 Case Class 或者元组就使用 `Encoders.product` 方法; +- 基本类型就使用其对应名称的方法,如 `scalaByte `,`scalaFloat`,`scalaShort` 等,示例如下: + +```scala +override def bufferEncoder: Encoder[SumAndCount] = Encoders.product +override def outputEncoder: Encoder[Double] = Encoders.scalaDouble +``` + + + +### 3.2 无类型的自定义聚合函数 + +理解了有类型的自定义聚合函数后,无类型的定义方式也基本相同,代码如下: + +```scala +import org.apache.spark.sql.expressions.{MutableAggregationBuffer, UserDefinedAggregateFunction} +import org.apache.spark.sql.types._ +import org.apache.spark.sql.{Row, SparkSession} + +object MyAverage extends UserDefinedAggregateFunction { + // 1.聚合操作输入参数的类型,字段名称可以自定义 + def inputSchema: StructType = StructType(StructField("MyInputColumn", LongType) :: Nil) + + // 2.聚合操作中间值的类型,字段名称可以自定义 + def bufferSchema: StructType = { + StructType(StructField("sum", LongType) :: StructField("MyCount", LongType) :: Nil) + } + + // 3.聚合操作输出参数的类型 + def dataType: DataType = DoubleType + + // 4.此函数是否始终在相同输入上返回相同的输出,通常为 true + def deterministic: Boolean = true + + // 5.定义零值 + def initialize(buffer: MutableAggregationBuffer): Unit = { + buffer(0) = 0L + buffer(1) = 0L + } + + // 6.同一分区中的 reduce 操作 + def update(buffer: MutableAggregationBuffer, input: Row): Unit = { + if (!input.isNullAt(0)) { + buffer(0) = buffer.getLong(0) + input.getLong(0) + buffer(1) = buffer.getLong(1) + 1 + } + } + + // 7.不同分区中的 merge 操作 + def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { + buffer1(0) = buffer1.getLong(0) + buffer2.getLong(0) + buffer1(1) = buffer1.getLong(1) + buffer2.getLong(1) + } + + // 8.计算最终的输出值 + def evaluate(buffer: Row): Double = buffer.getLong(0).toDouble / buffer.getLong(1) +} + +object SparkSqlApp { + + // 测试方法 + def main(args: Array[String]): Unit = { + + val spark = SparkSession.builder().appName("Spark-SQL").master("local[2]").getOrCreate() + // 9.注册自定义的聚合函数 + spark.udf.register("myAverage", MyAverage) + + val df = spark.read.json("file/emp.json") + df.createOrReplaceTempView("emp") + + // 10.使用自定义函数和内置函数分别进行计算 + val myAvg = spark.sql("SELECT myAverage(sal) as avg_sal FROM emp").first() + val avg = spark.sql("SELECT avg(sal) as avg_sal FROM emp").first() + + println("自定义 average 函数 : " + myAvg) + println("内置的 average 函数 : " + avg) + } +} +``` + + + +## 参考资料 + +1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\350\201\224\347\273\223\346\223\215\344\275\234.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\350\201\224\347\273\223\346\223\215\344\275\234.md" new file mode 100644 index 0000000..9eeced7 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/SparkSQL\350\201\224\347\273\223\346\223\215\344\275\234.md" @@ -0,0 +1,185 @@ +# Spark SQL JOIN + + + +## 一、 数据准备 + +本文主要介绍 Spark SQL 的多表连接,需要预先准备测试数据。分别创建员工和部门的 Datafame,并注册为临时视图,代码如下: + +```scala +val spark = SparkSession.builder().appName("aggregations").master("local[2]").getOrCreate() + +val empDF = spark.read.json("/usr/file/json/emp.json") +empDF.createOrReplaceTempView("emp") + +val deptDF = spark.read.json("/usr/file/json/dept.json") +deptDF.createOrReplaceTempView("dept") +``` + +两表的主要字段如下: + +```properties +emp 员工表 + |-- ENAME: 员工姓名 + |-- DEPTNO: 部门编号 + |-- EMPNO: 员工编号 + |-- HIREDATE: 入职时间 + |-- JOB: 职务 + |-- MGR: 上级编号 + |-- SAL: 薪资 + |-- COMM: 奖金 +``` + +```properties +dept 部门表 + |-- DEPTNO: 部门编号 + |-- DNAME: 部门名称 + |-- LOC: 部门所在城市 +``` + +> 注:emp.json,dept.json 可以在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录进行下载。 + + + +## 二、连接类型 + +Spark 中支持多种连接类型: + ++ **Inner Join** : 内连接; ++ **Full Outer Join** : 全外连接; ++ **Left Outer Join** : 左外连接; ++ **Right Outer Join** : 右外连接; ++ **Left Semi Join** : 左半连接; ++ **Left Anti Join** : 左反连接; ++ **Natural Join** : 自然连接; ++ **Cross (or Cartesian) Join** : 交叉 (或笛卡尔) 连接。 + +其中内,外连接,笛卡尔积均与普通关系型数据库中的相同,如下图所示: + +
+ +这里解释一下左半连接和左反连接,这两个连接等价于关系型数据库中的 `IN` 和 `NOT IN` 字句: + +```sql +-- LEFT SEMI JOIN +SELECT * FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno +-- 等价于如下的 IN 语句 +SELECT * FROM emp WHERE deptno IN (SELECT deptno FROM dept) + +-- LEFT ANTI JOIN +SELECT * FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno +-- 等价于如下的 IN 语句 +SELECT * FROM emp WHERE deptno NOT IN (SELECT deptno FROM dept) +``` + +所有连接类型的示例代码如下: + +### 2.1 INNER JOIN + +```scala +// 1.定义连接表达式 +val joinExpression = empDF.col("deptno") === deptDF.col("deptno") +// 2.连接查询 +empDF.join(deptDF,joinExpression).select("ename","dname").show() + +// 等价 SQL 如下: +spark.sql("SELECT ename,dname FROM emp JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.2 FULL OUTER JOIN + +```scala +empDF.join(deptDF, joinExpression, "outer").show() +spark.sql("SELECT * FROM emp FULL OUTER JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.3 LEFT OUTER JOIN + +```scala +empDF.join(deptDF, joinExpression, "left_outer").show() +spark.sql("SELECT * FROM emp LEFT OUTER JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.4 RIGHT OUTER JOIN + +```scala +empDF.join(deptDF, joinExpression, "right_outer").show() +spark.sql("SELECT * FROM emp RIGHT OUTER JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.5 LEFT SEMI JOIN + +```scala +empDF.join(deptDF, joinExpression, "left_semi").show() +spark.sql("SELECT * FROM emp LEFT SEMI JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.6 LEFT ANTI JOIN + +```scala +empDF.join(deptDF, joinExpression, "left_anti").show() +spark.sql("SELECT * FROM emp LEFT ANTI JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.7 CROSS JOIN + +```scala +empDF.join(deptDF, joinExpression, "cross").show() +spark.sql("SELECT * FROM emp CROSS JOIN dept ON emp.deptno = dept.deptno").show() +``` + +### 2.8 NATURAL JOIN + +自然连接是在两张表中寻找那些数据类型和列名都相同的字段,然后自动地将他们连接起来,并返回所有符合条件的结果。 + +```scala +spark.sql("SELECT * FROM emp NATURAL JOIN dept").show() +``` + +以下是一个自然连接的查询结果,程序自动推断出使用两张表都存在的 dept 列进行连接,其实际等价于: + +```sql +spark.sql("SELECT * FROM emp JOIN dept ON emp.deptno = dept.deptno").show() +``` + +
+ +由于自然连接常常会产生不可预期的结果,所以并不推荐使用。 + + + +## 三、连接的执行 + +在对大表与大表之间进行连接操作时,通常都会触发 `Shuffle Join`,两表的所有分区节点会进行 `All-to-All` 的通讯,这种查询通常比较昂贵,会对网络 IO 会造成比较大的负担。 + +
+ + + +而对于大表和小表的连接操作,Spark 会在一定程度上进行优化,如果小表的数据量小于 Worker Node 的内存空间,Spark 会考虑将小表的数据广播到每一个 Worker Node,在每个工作节点内部执行连接计算,这可以降低网络的 IO,但会加大每个 Worker Node 的 CPU 负担。 + +
+ +是否采用广播方式进行 `Join` 取决于程序内部对小表的判断,如果想明确使用广播方式进行 `Join`,则可以在 DataFrame API 中使用 `broadcast` 方法指定需要广播的小表: + +```scala +empDF.join(broadcast(deptDF), joinExpression).show() +``` + + + +## 参考资料 + +1. Matei Zaharia, Bill Chambers . Spark: The Definitive Guide[M] . 2018-02 diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_RDD.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_RDD.md" new file mode 100644 index 0000000..21e9e90 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_RDD.md" @@ -0,0 +1,237 @@ + + +# 弹性式数据集RDDs + + + +## 一、RDD简介 + +`RDD` 全称为 Resilient Distributed Datasets,是 Spark 最基本的数据抽象,它是只读的、分区记录的集合,支持并行操作,可以由外部数据集或其他 RDD 转换而来,它具有以下特性: + ++ 一个 RDD 由一个或者多个分区(Partitions)组成。对于 RDD 来说,每个分区会被一个计算任务所处理,用户可以在创建 RDD 时指定其分区个数,如果没有指定,则默认采用程序所分配到的 CPU 的核心数; ++ RDD 拥有一个用于计算分区的函数 compute; ++ RDD 会保存彼此间的依赖关系,RDD 的每次转换都会生成一个新的依赖关系,这种 RDD 之间的依赖关系就像流水线一样。在部分分区数据丢失后,可以通过这种依赖关系重新计算丢失的分区数据,而不是对 RDD 的所有分区进行重新计算; ++ Key-Value 型的 RDD 还拥有 Partitioner(分区器),用于决定数据被存储在哪个分区中,目前 Spark 中支持 HashPartitioner(按照哈希分区) 和 RangeParationer(按照范围进行分区); ++ 一个优先位置列表 (可选),用于存储每个分区的优先位置 (prefered location)。对于一个 HDFS 文件来说,这个列表保存的就是每个分区所在的块的位置,按照“移动数据不如移动计算“的理念,Spark 在进行任务调度的时候,会尽可能的将计算任务分配到其所要处理数据块的存储位置。 + +`RDD[T]` 抽象类的部分相关代码如下: + +```scala +// 由子类实现以计算给定分区 +def compute(split: Partition, context: TaskContext): Iterator[T] + +// 获取所有分区 +protected def getPartitions: Array[Partition] + +// 获取所有依赖关系 +protected def getDependencies: Seq[Dependency[_]] = deps + +// 获取优先位置列表 +protected def getPreferredLocations(split: Partition): Seq[String] = Nil + +// 分区器 由子类重写以指定它们的分区方式 +@transient val partitioner: Option[Partitioner] = None +``` + + + +## 二、创建RDD + +RDD 有两种创建方式,分别介绍如下: + +### 2.1 由现有集合创建 + +这里使用 `spark-shell` 进行测试,启动命令如下: + +```shell +spark-shell --master local[4] +``` + +启动 `spark-shell` 后,程序会自动创建应用上下文,相当于执行了下面的 Scala 语句: + +```scala +val conf = new SparkConf().setAppName("Spark shell").setMaster("local[4]") +val sc = new SparkContext(conf) +``` + +由现有集合创建 RDD,你可以在创建时指定其分区个数,如果没有指定,则采用程序所分配到的 CPU 的核心数: + +```scala +val data = Array(1, 2, 3, 4, 5) +// 由现有集合创建 RDD,默认分区数为程序所分配到的 CPU 的核心数 +val dataRDD = sc.parallelize(data) +// 查看分区数 +dataRDD.getNumPartitions +// 明确指定分区数 +val dataRDD = sc.parallelize(data,2) +``` + +执行结果如下: + +
+ +### 2.2 引用外部存储系统中的数据集 + +引用外部存储系统中的数据集,例如本地文件系统,HDFS,HBase 或支持 Hadoop InputFormat 的任何数据源。 + +```scala +val fileRDD = sc.textFile("/usr/file/emp.txt") +// 获取第一行文本 +fileRDD.take(1) +``` + +使用外部存储系统时需要注意以下两点: + ++ 如果在集群环境下从本地文件系统读取数据,则要求该文件必须在集群中所有机器上都存在,且路径相同; ++ 支持目录路径,支持压缩文件,支持使用通配符。 + +### 2.3 textFile & wholeTextFiles + +两者都可以用来读取外部文件,但是返回格式是不同的: + ++ **textFile**:其返回格式是 `RDD[String]` ,返回的是就是文件内容,RDD 中每一个元素对应一行数据; ++ **wholeTextFiles**:其返回格式是 `RDD[(String, String)]`,元组中第一个参数是文件路径,第二个参数是文件内容; ++ 两者都提供第二个参数来控制最小分区数; ++ 从 HDFS 上读取文件时,Spark 会为每个块创建一个分区。 + +```scala +def textFile(path: String,minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {...} +def wholeTextFiles(path: String,minPartitions: Int = defaultMinPartitions): RDD[(String, String)]={..} +``` + + + +## 三、操作RDD + +RDD 支持两种类型的操作:*transformations*(转换,从现有数据集创建新数据集)和 *actions*(在数据集上运行计算后将值返回到驱动程序)。RDD 中的所有转换操作都是惰性的,它们只是记住这些转换操作,但不会立即执行,只有遇到 *action* 操作后才会真正的进行计算,这类似于函数式编程中的惰性求值。 + +```scala +val list = List(1, 2, 3) +// map 是一个 transformations 操作,而 foreach 是一个 actions 操作 +sc.parallelize(list).map(_ * 10).foreach(println) +// 输出: 10 20 30 +``` + + + +## 四、缓存RDD + +### 4.1 缓存级别 + +Spark 速度非常快的一个原因是 RDD 支持缓存。成功缓存后,如果之后的操作使用到了该数据集,则直接从缓存中获取。虽然缓存也有丢失的风险,但是由于 RDD 之间的依赖关系,如果某个分区的缓存数据丢失,只需要重新计算该分区即可。 + +Spark 支持多种缓存级别 : + +| Storage Level
(存储级别) | Meaning(含义) | +| ---------------------------------------------- | ------------------------------------------------------------ | +| `MEMORY_ONLY` | 默认的缓存级别,将 RDD 以反序列化的 Java 对象的形式存储在 JVM 中。如果内存空间不够,则部分分区数据将不再缓存。 | +| `MEMORY_AND_DISK` | 将 RDD 以反序列化的 Java 对象的形式存储 JVM 中。如果内存空间不够,将未缓存的分区数据存储到磁盘,在需要使用这些分区时从磁盘读取。 | +| `MEMORY_ONLY_SER`
| 将 RDD 以序列化的 Java 对象的形式进行存储(每个分区为一个 byte 数组)。这种方式比反序列化对象节省存储空间,但在读取时会增加 CPU 的计算负担。仅支持 Java 和 Scala 。 | +| `MEMORY_AND_DISK_SER`
| 类似于 `MEMORY_ONLY_SER`,但是溢出的分区数据会存储到磁盘,而不是在用到它们时重新计算。仅支持 Java 和 Scala。 | +| `DISK_ONLY` | 只在磁盘上缓存 RDD | +| `MEMORY_ONLY_2`,
`MEMORY_AND_DISK_2`, etc | 与上面的对应级别功能相同,但是会为每个分区在集群中的两个节点上建立副本。 | +| `OFF_HEAP` | 与 `MEMORY_ONLY_SER` 类似,但将数据存储在堆外内存中。这需要启用堆外内存。 | + +> 启动堆外内存需要配置两个参数: +> +> + **spark.memory.offHeap.enabled** :是否开启堆外内存,默认值为 false,需要设置为 true; +> + **spark.memory.offHeap.size** : 堆外内存空间的大小,默认值为 0,需要设置为正值。 + +### 4.2 使用缓存 + +缓存数据的方法有两个:`persist` 和 `cache` 。`cache` 内部调用的也是 `persist`,它是 `persist` 的特殊化形式,等价于 `persist(StorageLevel.MEMORY_ONLY)`。示例如下: + +```scala +// 所有存储级别均定义在 StorageLevel 对象中 +fileRDD.persist(StorageLevel.MEMORY_AND_DISK) +fileRDD.cache() +``` + +### 4.3 移除缓存 + +Spark 会自动监视每个节点上的缓存使用情况,并按照最近最少使用(LRU)的规则删除旧数据分区。当然,你也可以使用 `RDD.unpersist()` 方法进行手动删除。 + + + +## 五、理解shuffle + +### 5.1 shuffle介绍 + +在 Spark 中,一个任务对应一个分区,通常不会跨分区操作数据。但如果遇到 `reduceByKey` 等操作,Spark 必须从所有分区读取数据,并查找所有键的所有值,然后汇总在一起以计算每个键的最终结果 ,这称为 `Shuffle`。 + +
+ + + +### 5.2 Shuffle的影响 + +Shuffle 是一项昂贵的操作,因为它通常会跨节点操作数据,这会涉及磁盘 I/O,网络 I/O,和数据序列化。某些 Shuffle 操作还会消耗大量的堆内存,因为它们使用堆内存来临时存储需要网络传输的数据。Shuffle 还会在磁盘上生成大量中间文件,从 Spark 1.3 开始,这些文件将被保留,直到相应的 RDD 不再使用并进行垃圾回收,这样做是为了避免在计算时重复创建 Shuffle 文件。如果应用程序长期保留对这些 RDD 的引用,则垃圾回收可能在很长一段时间后才会发生,这意味着长时间运行的 Spark 作业可能会占用大量磁盘空间,通常可以使用 `spark.local.dir` 参数来指定这些临时文件的存储目录。 + +### 5.3 导致Shuffle的操作 + +由于 Shuffle 操作对性能的影响比较大,所以需要特别注意使用,以下操作都会导致 Shuffle: + ++ **涉及到重新分区操作**: 如 `repartition` 和 `coalesce`; ++ **所有涉及到 ByKey 的操作**:如 `groupByKey` 和 `reduceByKey`,但 `countByKey` 除外; ++ **联结操作**:如 `cogroup` 和 `join`。 + + + +## 五、宽依赖和窄依赖 + +RDD 和它的父 RDD(s) 之间的依赖关系分为两种不同的类型: + +- **窄依赖 (narrow dependency)**:父 RDDs 的一个分区最多被子 RDDs 一个分区所依赖; +- **宽依赖 (wide dependency)**:父 RDDs 的一个分区可以被子 RDDs 的多个子分区所依赖。 + +如下图,每一个方框表示一个 RDD,带有颜色的矩形表示分区: + +
+ + + +区分这两种依赖是非常有用的: + ++ 首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)对父分区数据进行计算,例如先执行 map 操作,然后执行 filter 操作。而宽依赖则需要计算好所有父分区的数据,然后再在节点之间进行 Shuffle,这与 MapReduce 类似。 ++ 窄依赖能够更有效地进行数据恢复,因为只需重新对丢失分区的父分区进行计算,且不同节点之间可以并行计算;而对于宽依赖而言,如果数据丢失,则需要对所有父分区数据进行计算并再次 Shuffle。 + + + +## 六、DAG的生成 + +RDD(s) 及其之间的依赖关系组成了 DAG(有向无环图),DAG 定义了这些 RDD(s) 之间的 Lineage(血统) 关系,通过血统关系,如果一个 RDD 的部分或者全部计算结果丢失了,也可以重新进行计算。那么 Spark 是如何根据 DAG 来生成计算任务呢?主要是根据依赖关系的不同将 DAG 划分为不同的计算阶段 (Stage): + ++ 对于窄依赖,由于分区的依赖关系是确定的,其转换操作可以在同一个线程执行,所以可以划分到同一个执行阶段; ++ 对于宽依赖,由于 Shuffle 的存在,只能在父 RDD(s) 被 Shuffle 处理完成后,才能开始接下来的计算,因此遇到宽依赖就需要重新划分阶段。 + +
+ + + + + +## 参考资料 + +1. 张安站 . Spark 技术内幕:深入解析 Spark 内核架构设计与实现原理[M] . 机械工业出版社 . 2015-09-01 +2. [RDD Programming Guide](https://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) +3. [RDD:基于内存的集群计算容错抽象](http://shiyanjun.cn/archives/744.html) + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\344\270\216\346\265\201\345\244\204\347\220\206.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\344\270\216\346\265\201\345\244\204\347\220\206.md" new file mode 100644 index 0000000..6c6dfce --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\344\270\216\346\265\201\345\244\204\347\220\206.md" @@ -0,0 +1,79 @@ +# Spark Streaming与流处理 + + + +## 一、流处理 + +### 1.1 静态数据处理 + +在流处理之前,数据通常存储在数据库,文件系统或其他形式的存储系统中。应用程序根据需要查询数据或计算数据。这就是传统的静态数据处理架构。Hadoop 采用 HDFS 进行数据存储,采用 MapReduce 进行数据查询或分析,这就是典型的静态数据处理架构。 + +
+ + + +### 1.2 流处理 + +而流处理则是直接对运动中的数据的处理,在接收数据时直接计算数据。 + +大多数数据都是连续的流:传感器事件,网站上的用户活动,金融交易等等 ,所有这些数据都是随着时间的推移而创建的。 + +接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动,同时具备可扩展性和容错能力,Storm 和 Flink 就是其代表性的实现。 + +
+ + + +流处理带来了静态数据处理所不具备的众多优点: + + + +- **应用程序立即对数据做出反应**:降低了数据的滞后性,使得数据更具有时效性,更能反映对未来的预期; +- **流处理可以处理更大的数据量**:直接处理数据流,并且只保留数据中有意义的子集,并将其传送到下一个处理单元,逐级过滤数据,降低需要处理的数据量,从而能够承受更大的数据量; +- **流处理更贴近现实的数据模型**:在实际的环境中,一切数据都是持续变化的,要想能够通过过去的数据推断未来的趋势,必须保证数据的不断输入和模型的不断修正,典型的就是金融市场、股票市场,流处理能更好的应对这些数据的连续性的特征和及时性的需求; +- **流处理分散和分离基础设施**:流式处理减少了对大型数据库的需求。相反,每个流处理程序通过流处理框架维护了自己的数据和状态,这使得流处理程序更适合微服务架构。 + + + +## 二、Spark Streaming + +### 2.1 简介 + +Spark Streaming 是 Spark 的一个子模块,用于快速构建可扩展,高吞吐量,高容错的流处理程序。具有以下特点: + ++ 通过高级 API 构建应用程序,简单易用; ++ 支持多种语言,如 Java,Scala 和 Python; ++ 良好的容错性,Spark Streaming 支持快速从失败中恢复丢失的操作状态; ++ 能够和 Spark 其他模块无缝集成,将流处理与批处理完美结合; ++ Spark Streaming 可以从 HDFS,Flume,Kafka,Twitter 和 ZeroMQ 读取数据,也支持自定义数据源。 + +
+ +### 2.2 DStream + +Spark Streaming 提供称为离散流 (DStream) 的高级抽象,用于表示连续的数据流。 DStream 可以从来自 Kafka,Flume 和 Kinesis 等数据源的输入数据流创建,也可以由其他 DStream 转化而来。**在内部,DStream 表示为一系列 RDD**。 + +
+ + + +### 2.3 Spark & Storm & Flink + +storm 和 Flink 都是真正意义上的流计算框架,但 Spark Streaming 只是将数据流进行极小粒度的拆分,拆分为多个批处理,使得其能够得到接近于流处理的效果,但其本质上还是批处理(或微批处理)。 + + + + + +## 参考资料 + +1. [Spark Streaming Programming Guide](https://spark.apache.org/docs/latest/streaming-programming-guide.html) +2. [What is stream processing?](https://www.ververica.com/what-is-stream-processing) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\345\237\272\346\234\254\346\223\215\344\275\234.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\345\237\272\346\234\254\346\223\215\344\275\234.md" new file mode 100644 index 0000000..da78264 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\345\237\272\346\234\254\346\223\215\344\275\234.md" @@ -0,0 +1,335 @@ +# Spark Streaming 基本操作 + + + +## 一、案例引入 + +这里先引入一个基本的案例来演示流的创建:获取指定端口上的数据并进行词频统计。项目依赖和代码实现如下: + +```xml + + org.apache.spark + spark-streaming_2.12 + 2.4.3 + +``` + +```scala +import org.apache.spark.SparkConf +import org.apache.spark.streaming.{Seconds, StreamingContext} + +object NetworkWordCount { + + def main(args: Array[String]) { + + /*指定时间间隔为 5s*/ + val sparkConf = new SparkConf().setAppName("NetworkWordCount").setMaster("local[2]") + val ssc = new StreamingContext(sparkConf, Seconds(5)) + + /*创建文本输入流,并进行词频统计*/ + val lines = ssc.socketTextStream("hadoop001", 9999) + lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _).print() + + /*启动服务*/ + ssc.start() + /*等待服务结束*/ + ssc.awaitTermination() + } +} +``` + +使用本地模式启动 Spark 程序,然后使用 `nc -lk 9999` 打开端口并输入测试数据: + +```shell +[root@hadoop001 ~]# nc -lk 9999 +hello world hello spark hive hive hadoop +storm storm flink azkaban +``` + +此时控制台输出如下,可以看到已经接收到数据并按行进行了词频统计。 + +
+
+ +下面针对示例代码进行讲解: + +### 3.1 StreamingContext + +Spark Streaming 编程的入口类是 StreamingContext,在创建时候需要指明 `sparkConf` 和 `batchDuration`(批次时间),Spark 流处理本质是将流数据拆分为一个个批次,然后进行微批处理,`batchDuration` 就是批次拆分的时间间隔。这个时间可以根据业务需求和服务器性能进行指定,如果业务要求低延迟并且服务器性能也允许,则这个时间可以指定得很短。 + +这里需要注意的是:示例代码使用的是本地模式,配置为 `local[2]`,这里不能配置为 `local[1]`。这是因为对于流数据的处理,Spark 必须有一个独立的 Executor 来接收数据,然后再由其他的 Executors 来处理,所以为了保证数据能够被处理,至少要有 2 个 Executors。这里我们的程序只有一个数据流,在并行读取多个数据流的时候,也需要保证有足够的 Executors 来接收和处理数据。 + +### 3.2 数据源 + +在示例代码中使用的是 `socketTextStream` 来创建基于 Socket 的数据流,实际上 Spark 还支持多种数据源,分为以下两类: + ++ **基本数据源**:包括文件系统、Socket 连接等; ++ **高级数据源**:包括 Kafka,Flume,Kinesis 等。 + +在基本数据源中,Spark 支持监听 HDFS 上指定目录,当有新文件加入时,会获取其文件内容作为输入流。创建方式如下: + +```scala +// 对于文本文件,指明监听目录即可 +streamingContext.textFileStream(dataDirectory) +// 对于其他文件,需要指明目录,以及键的类型、值的类型、和输入格式 +streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory) +``` + +被监听的目录可以是具体目录,如 `hdfs://host:8040/logs/`;也可以使用通配符,如 `hdfs://host:8040/logs/2017/*`。 + +> 关于高级数据源的整合单独整理至:[Spark Streaming 整合 Flume](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Flume.md) 和 [Spark Streaming 整合 Kafka](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Spark_Streaming整合Kafka.md) + +### 3.3 服务的启动与停止 + +在示例代码中,使用 `streamingContext.start()` 代表启动服务,此时还要使用 `streamingContext.awaitTermination()` 使服务处于等待和可用的状态,直到发生异常或者手动使用 `streamingContext.stop()` 进行终止。 + + + +## 二、Transformation + +### 2.1 DStream与RDDs + +DStream 是 Spark Streaming 提供的基本抽象。它表示连续的数据流。在内部,DStream 由一系列连续的 RDD 表示。所以从本质上而言,应用于 DStream 的任何操作都会转换为底层 RDD 上的操作。例如,在示例代码中 flatMap 算子的操作实际上是作用在每个 RDDs 上 (如下图)。因为这个原因,所以 DStream 能够支持 RDD 大部分的*transformation*算子。 + +
+ +### 2.2 updateStateByKey + +除了能够支持 RDD 的算子外,DStream 还有部分独有的*transformation*算子,这当中比较常用的是 `updateStateByKey`。文章开头的词频统计程序,只能统计每一次输入文本中单词出现的数量,想要统计所有历史输入中单词出现的数量,可以使用 `updateStateByKey` 算子。代码如下: + +```scala +object NetworkWordCountV2 { + + + def main(args: Array[String]) { + + /* + * 本地测试时最好指定 hadoop 用户名,否则会默认使用本地电脑的用户名, + * 此时在 HDFS 上创建目录时可能会抛出权限不足的异常 + */ + System.setProperty("HADOOP_USER_NAME", "root") + + val sparkConf = new SparkConf().setAppName("NetworkWordCountV2").setMaster("local[2]") + val ssc = new StreamingContext(sparkConf, Seconds(5)) + /*必须要设置检查点*/ + ssc.checkpoint("hdfs://hadoop001:8020/spark-streaming") + val lines = ssc.socketTextStream("hadoop001", 9999) + lines.flatMap(_.split(" ")).map(x => (x, 1)) + .updateStateByKey[Int](updateFunction _) //updateStateByKey 算子 + .print() + + ssc.start() + ssc.awaitTermination() + } + + /** + * 累计求和 + * + * @param currentValues 当前的数据 + * @param preValues 之前的数据 + * @return 相加后的数据 + */ + def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = { + val current = currentValues.sum + val pre = preValues.getOrElse(0) + Some(current + pre) + } +} +``` + +使用 `updateStateByKey` 算子,你必须使用 `ssc.checkpoint()` 设置检查点,这样当使用 `updateStateByKey` 算子时,它会去检查点中取出上一次保存的信息,并使用自定义的 `updateFunction` 函数将上一次的数据和本次数据进行相加,然后返回。 + +### 2.3 启动测试 + +在监听端口输入如下测试数据: + +```shell +[root@hadoop001 ~]# nc -lk 9999 +hello world hello spark hive hive hadoop +storm storm flink azkaban +hello world hello spark hive hive hadoop +storm storm flink azkaban +``` + +此时控制台输出如下,所有输入都被进行了词频累计: + +
+同时在输出日志中还可以看到检查点操作的相关信息: + +```shell +# 保存检查点信息 +19/05/27 16:21:05 INFO CheckpointWriter: Saving checkpoint for time 1558945265000 ms +to file 'hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000' + +# 删除已经无用的检查点信息 +19/05/27 16:21:30 INFO CheckpointWriter: +Deleting hdfs://hadoop001:8020/spark-streaming/checkpoint-1558945265000 +``` + +## 三、输出操作 + +### 3.1 输出API + +Spark Streaming 支持以下输出操作: + +| Output Operation | Meaning | +| :------------------------------------------ | :----------------------------------------------------------- | +| **print**() | 在运行流应用程序的 driver 节点上打印 DStream 中每个批次的前十个元素。用于开发调试。 | +| **saveAsTextFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容保存为文本文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **saveAsObjectFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容序列化为 Java 对象,并保存到 SequenceFiles。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **saveAsHadoopFiles**(*prefix*, [*suffix*]) | 将 DStream 的内容保存为 Hadoop 文件。每个批处理间隔的文件名基于前缀和后缀生成:“prefix-TIME_IN_MS [.suffix]”。 | +| **foreachRDD**(*func*) | 最通用的输出方式,它将函数 func 应用于从流生成的每个 RDD。此函数应将每个 RDD 中的数据推送到外部系统,例如将 RDD 保存到文件,或通过网络将其写入数据库。 | + +前面的四个 API 都是直接调用即可,下面主要讲解通用的输出方式 `foreachRDD(func)`,通过该 API 你可以将数据保存到任何你需要的数据源。 + +### 3.1 foreachRDD + +这里我们使用 Redis 作为客户端,对文章开头示例程序进行改变,把每一次词频统计的结果写入到 Redis,并利用 Redis 的 `HINCRBY` 命令来进行词频统计。这里需要导入 Jedis 依赖: + +```xml + + redis.clients + jedis + 2.9.0 + +``` + +具体实现代码如下: + +```scala +import org.apache.spark.SparkConf +import org.apache.spark.streaming.dstream.DStream +import org.apache.spark.streaming.{Seconds, StreamingContext} +import redis.clients.jedis.Jedis + +object NetworkWordCountToRedis { + + def main(args: Array[String]) { + + val sparkConf = new SparkConf().setAppName("NetworkWordCountToRedis").setMaster("local[2]") + val ssc = new StreamingContext(sparkConf, Seconds(5)) + + /*创建文本输入流,并进行词频统计*/ + val lines = ssc.socketTextStream("hadoop001", 9999) + val pairs: DStream[(String, Int)] = lines.flatMap(_.split(" ")).map(x => (x, 1)).reduceByKey(_ + _) + /*保存数据到 Redis*/ + pairs.foreachRDD { rdd => + rdd.foreachPartition { partitionOfRecords => + var jedis: Jedis = null + try { + jedis = JedisPoolUtil.getConnection + partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2)) + } catch { + case ex: Exception => + ex.printStackTrace() + } finally { + if (jedis != null) jedis.close() + } + } + } + ssc.start() + ssc.awaitTermination() + } +} + +``` + +其中 `JedisPoolUtil` 的代码如下: + +```java +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +public class JedisPoolUtil { + + /* 声明为 volatile 防止指令重排序 */ + private static volatile JedisPool jedisPool = null; + private static final String HOST = "localhost"; + private static final int PORT = 6379; + + /* 双重检查锁实现懒汉式单例 */ + public static Jedis getConnection() { + if (jedisPool == null) { + synchronized (JedisPoolUtil.class) { + if (jedisPool == null) { + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxTotal(30); + config.setMaxIdle(10); + jedisPool = new JedisPool(config, HOST, PORT); + } + } + } + return jedisPool.getResource(); + } +} +``` + +### 3.3 代码说明 + +这里将上面保存到 Redis 的代码单独抽取出来,并去除异常判断的部分。精简后的代码如下: + +```scala +pairs.foreachRDD { rdd => + rdd.foreachPartition { partitionOfRecords => + val jedis = JedisPoolUtil.getConnection + partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2)) + jedis.close() + } +} +``` + +这里可以看到一共使用了三次循环,分别是循环 RDD,循环分区,循环每条记录,上面我们的代码是在循环分区的时候获取连接,也就是为每一个分区获取一个连接。但是这里大家可能会有疑问:为什么不在循环 RDD 的时候,为每一个 RDD 获取一个连接,这样所需要的连接数会更少。实际上这是不可行的,如果按照这种情况进行改写,如下: + +```scala +pairs.foreachRDD { rdd => + val jedis = JedisPoolUtil.getConnection + rdd.foreachPartition { partitionOfRecords => + partitionOfRecords.foreach(record => jedis.hincrBy("wordCount", record._1, record._2)) + } + jedis.close() +} +``` + +此时在执行时候就会抛出 `Caused by: java.io.NotSerializableException: redis.clients.jedis.Jedis`,这是因为在实际计算时,Spark 会将对 RDD 操作分解为多个 Task,Task 运行在具体的 Worker Node 上。在执行之前,Spark 会对任务进行闭包,之后闭包被序列化并发送给每个 Executor,而 `Jedis` 显然是不能被序列化的,所以会抛出异常。 + +第二个需要注意的是 ConnectionPool 最好是一个静态,惰性初始化连接池 。这是因为 Spark 的转换操作本身就是惰性的,且没有数据流时不会触发写出操作,所以出于性能考虑,连接池应该是惰性的,因此上面 `JedisPool` 在初始化时采用了懒汉式单例进行惰性初始化。 + +### 3.4 启动测试 + +在监听端口输入如下测试数据: + +```shell +[root@hadoop001 ~]# nc -lk 9999 +hello world hello spark hive hive hadoop +storm storm flink azkaban +hello world hello spark hive hive hadoop +storm storm flink azkaban +``` + +使用 Redis Manager 查看写入结果 (如下图),可以看到与使用 `updateStateByKey` 算子得到的计算结果相同。 + +
+
+ +> 本片文章所有源码见本仓库:[spark-streaming-basis](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-basis) + + + +## 参考资料 + +Spark 官方文档:http://spark.apache.org/docs/latest/streaming-programming-guide.html diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Flume.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Flume.md" new file mode 100644 index 0000000..fd40d08 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Flume.md" @@ -0,0 +1,359 @@ +# Spark Streaming 整合 Flume + + + + +## 一、简介 + +Apache Flume 是一个分布式,高可用的数据收集系统,可以从不同的数据源收集数据,经过聚合后发送到分布式计算框架或者存储系统中。Spark Straming 提供了以下两种方式用于 Flume 的整合。 + +## 二、推送式方法 + +在推送式方法 (Flume-style Push-based Approach) 中,Spark Streaming 程序需要对某台服务器的某个端口进行监听,Flume 通过 `avro Sink` 将数据源源不断推送到该端口。这里以监听日志文件为例,具体整合方式如下: + +### 2.1 配置日志收集Flume + +新建配置 `netcat-memory-avro.properties`,使用 `tail` 命令监听文件内容变化,然后将新的文件内容通过 `avro sink` 发送到 hadoop001 这台服务器的 8888 端口: + +```properties +#指定agent的sources,sinks,channels +a1.sources = s1 +a1.sinks = k1 +a1.channels = c1 + +#配置sources属性 +a1.sources.s1.type = exec +a1.sources.s1.command = tail -F /tmp/log.txt +a1.sources.s1.shell = /bin/bash -c +a1.sources.s1.channels = c1 + +#配置sink +a1.sinks.k1.type = avro +a1.sinks.k1.hostname = hadoop001 +a1.sinks.k1.port = 8888 +a1.sinks.k1.batch-size = 1 +a1.sinks.k1.channel = c1 + +#配置channel类型 +a1.channels.c1.type = memory +a1.channels.c1.capacity = 1000 +a1.channels.c1.transactionCapacity = 100 +``` + +### 2.2 项目依赖 + +项目采用 Maven 工程进行构建,主要依赖为 `spark-streaming` 和 `spark-streaming-flume`。 + +```xml + + 2.11 + 2.4.0 + + + + + + org.apache.spark + spark-streaming_${scala.version} + ${spark.version} + + + + org.apache.spark + spark-streaming-flume_${scala.version} + 2.4.3 + + + +``` + +### 2.3 Spark Streaming接收日志数据 + +调用 FlumeUtils 工具类的 `createStream` 方法,对 hadoop001 的 8888 端口进行监听,获取到流数据并进行打印: + +```scala +import org.apache.spark.SparkConf +import org.apache.spark.streaming.{Seconds, StreamingContext} +import org.apache.spark.streaming.flume.FlumeUtils + +object PushBasedWordCount { + + def main(args: Array[String]): Unit = { + val sparkConf = new SparkConf() + val ssc = new StreamingContext(sparkConf, Seconds(5)) + // 1.获取输入流 + val flumeStream = FlumeUtils.createStream(ssc, "hadoop001", 8888) + // 2.打印输入流的数据 + flumeStream.map(line => new String(line.event.getBody.array()).trim).print() + + ssc.start() + ssc.awaitTermination() + } +} +``` + +### 2.4 项目打包 + +因为 Spark 安装目录下是不含有 `spark-streaming-flume` 依赖包的,所以在提交到集群运行时候必须提供该依赖包,你可以在提交命令中使用 `--jar` 指定上传到服务器的该依赖包,或者使用 `--packages org.apache.spark:spark-streaming-flume_2.12:2.4.3` 指定依赖包的完整名称,这样程序在启动时会先去中央仓库进行下载。 + +这里我采用的是第三种方式:使用 `maven-shade-plugin` 插件进行 `ALL IN ONE` 打包,把所有依赖的 Jar 一并打入最终包中。需要注意的是 `spark-streaming` 包在 Spark 安装目录的 `jars` 目录中已经提供,所以不需要打入。插件配置如下: + + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.sf + META-INF/*.DSA + META-INF/*.dsa + META-INF/*.RSA + META-INF/*.rsa + META-INF/*.EC + META-INF/*.ec + META-INF/MSFTSIG.SF + META-INF/MSFTSIG.RSA + + + + + + org.apache.spark:spark-streaming_${scala.version} + org.scala-lang:scala-library + org.apache.commons:commons-lang3 + + + + + + package + + shade + + + + + + + + + + + + + + org.scala-tools + maven-scala-plugin + 2.15.1 + + + scala-compile + + compile + + + + **/*.scala + + + + + scala-test-compile + + testCompile + + + + + + +``` +> 本项目完整源码见:[spark-streaming-flume](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-flume) + +使用 `mvn clean package` 命令打包后会生产以下两个 Jar 包,提交 ` 非 original` 开头的 Jar 即可。 + +
+ +### 2.5 启动服务和提交作业 + + 启动 Flume 服务: + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-avro.properties \ +--name a1 -Dflume.root.logger=INFO,console +``` + +提交 Spark Streaming 作业: + +```shell +spark-submit \ +--class com.heibaiying.flume.PushBasedWordCount \ +--master local[4] \ +/usr/appjar/spark-streaming-flume-1.0.jar +``` + +### 2.6 测试 + +这里使用 `echo` 命令模拟日志产生的场景,往日志文件中追加数据,然后查看程序的输出: + +
+ +Spark Streaming 程序成功接收到数据并打印输出: + +
+ +### 2.7 注意事项 + +#### 1. 启动顺序 + +这里需要注意的,不论你先启动 Spark 程序还是 Flume 程序,由于两者的启动都需要一定的时间,此时先启动的程序会短暂地抛出端口拒绝连接的异常,此时不需要进行任何操作,等待两个程序都启动完成即可。 + +
+ +#### 2. 版本一致 + +最好保证用于本地开发和编译的 Scala 版本和 Spark 的 Scala 版本一致,至少保证大版本一致,如都是 `2.11`。 + +
+ +## 三、拉取式方法 + +拉取式方法 (Pull-based Approach using a Custom Sink) 是将数据推送到 `SparkSink` 接收器中,此时数据会保持缓冲状态,Spark Streaming 定时从接收器中拉取数据。这种方式是基于事务的,即只有在 Spark Streaming 接收和复制数据完成后,才会删除缓存的数据。与第一种方式相比,具有更强的可靠性和容错保证。整合步骤如下: + +### 3.1 配置日志收集Flume + +新建 Flume 配置文件 `netcat-memory-sparkSink.properties`,配置和上面基本一致,只是把 `a1.sinks.k1.type` 的属性修改为 `org.apache.spark.streaming.flume.sink.SparkSink`,即采用 Spark 接收器。 + +```properties +#指定agent的sources,sinks,channels +a1.sources = s1 +a1.sinks = k1 +a1.channels = c1 + +#配置sources属性 +a1.sources.s1.type = exec +a1.sources.s1.command = tail -F /tmp/log.txt +a1.sources.s1.shell = /bin/bash -c +a1.sources.s1.channels = c1 + +#配置sink +a1.sinks.k1.type = org.apache.spark.streaming.flume.sink.SparkSink +a1.sinks.k1.hostname = hadoop001 +a1.sinks.k1.port = 8888 +a1.sinks.k1.batch-size = 1 +a1.sinks.k1.channel = c1 + +#配置channel类型 +a1.channels.c1.type = memory +a1.channels.c1.capacity = 1000 +a1.channels.c1.transactionCapacity = 100 +``` + +### 2.2 新增依赖 + +使用拉取式方法需要额外添加以下两个依赖: + +```xml + + org.scala-lang + scala-library + 2.12.8 + + + org.apache.commons + commons-lang3 + 3.5 + +``` + +注意:添加这两个依赖只是为了本地测试,Spark 的安装目录下已经提供了这两个依赖,所以在最终打包时需要进行排除。 + +### 2.3 Spark Streaming接收日志数据 + +这里和上面推送式方法的代码基本相同,只是将调用方法改为 `createPollingStream`。 + +```scala +import org.apache.spark.SparkConf +import org.apache.spark.streaming.{Seconds, StreamingContext} +import org.apache.spark.streaming.flume.FlumeUtils + +object PullBasedWordCount { + + def main(args: Array[String]): Unit = { + + val sparkConf = new SparkConf() + val ssc = new StreamingContext(sparkConf, Seconds(5)) + // 1.获取输入流 + val flumeStream = FlumeUtils.createPollingStream(ssc, "hadoop001", 8888) + // 2.打印输入流中的数据 + flumeStream.map(line => new String(line.event.getBody.array()).trim).print() + ssc.start() + ssc.awaitTermination() + } +} +``` + +### 2.4 启动测试 + +启动和提交作业流程与上面相同,这里给出执行脚本,过程不再赘述。 + +启动 Flume 进行日志收集: + +```shell +flume-ng agent \ +--conf conf \ +--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-sparkSink.properties \ +--name a1 -Dflume.root.logger=INFO,console +``` + +提交 Spark Streaming 作业: + +```shel +spark-submit \ +--class com.heibaiying.flume.PullBasedWordCount \ +--master local[4] \ +/usr/appjar/spark-streaming-flume-1.0.jar +``` + + + +## 参考资料 + +- [streaming-flume-integration](https://spark.apache.org/docs/latest/streaming-flume-integration.html) +- 关于大数据应用常用的打包方式可以参见:[大数据应用常用打包方式](https://github.com/heibaiying/BigData-Notes/blob/master/notes/大数据应用常用打包方式.md) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Kafka.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Kafka.md" new file mode 100644 index 0000000..139a70f --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Streaming\346\225\264\345\220\210Kafka.md" @@ -0,0 +1,321 @@ +# Spark Streaming 整合 Kafka + + + + +## 一、版本说明 + +Spark 针对 Kafka 的不同版本,提供了两套整合方案:`spark-streaming-kafka-0-8` 和 `spark-streaming-kafka-0-10`,其主要区别如下: + +| | [spark-streaming-kafka-0-8](https://spark.apache.org/docs/latest/streaming-kafka-0-8-integration.html) | [spark-streaming-kafka-0-10](https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html) | +| :-------------------------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- | +| Kafka 版本 | 0.8.2.1 or higher | 0.10.0 or higher | +| AP 状态 | Deprecated
从 Spark 2.3.0 版本开始,Kafka 0.8 支持已被弃用 | Stable(稳定版) | +| 语言支持 | Scala, Java, Python | Scala, Java | +| Receiver DStream | Yes | No | +| Direct DStream | Yes | Yes | +| SSL / TLS Support | No | Yes | +| Offset Commit API(偏移量提交) | No | Yes | +| Dynamic Topic Subscription
(动态主题订阅) | No | Yes | + +本文使用的 Kafka 版本为 `kafka_2.12-2.2.0`,故采用第二种方式进行整合。 + +## 二、项目依赖 + +项目采用 Maven 进行构建,主要依赖如下: + +```xml + + 2.12 + + + + + + org.apache.spark + spark-streaming_${scala.version} + ${spark.version} + + + + org.apache.spark + spark-streaming-kafka-0-10_${scala.version} + 2.4.3 + + +``` + +> 完整源码见本仓库:[spark-streaming-kafka](https://github.com/heibaiying/BigData-Notes/tree/master/code/spark/spark-streaming-kafka) + +## 三、整合Kafka + +通过调用 `KafkaUtils` 对象的 `createDirectStream` 方法来创建输入流,完整代码如下: + +```scala +import org.apache.kafka.common.serialization.StringDeserializer +import org.apache.spark.SparkConf +import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe +import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent +import org.apache.spark.streaming.kafka010._ +import org.apache.spark.streaming.{Seconds, StreamingContext} + +/** + * spark streaming 整合 kafka + */ +object KafkaDirectStream { + + def main(args: Array[String]): Unit = { + + val sparkConf = new SparkConf().setAppName("KafkaDirectStream").setMaster("local[2]") + val streamingContext = new StreamingContext(sparkConf, Seconds(5)) + + val kafkaParams = Map[String, Object]( + /* + * 指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找其他 broker 的信息。 + * 不过建议至少提供两个 broker 的信息作为容错。 + */ + "bootstrap.servers" -> "hadoop001:9092", + /*键的序列化器*/ + "key.deserializer" -> classOf[StringDeserializer], + /*值的序列化器*/ + "value.deserializer" -> classOf[StringDeserializer], + /*消费者所在分组的 ID*/ + "group.id" -> "spark-streaming-group", + /* + * 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: + * latest: 在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录) + * earliest: 在偏移量无效的情况下,消费者将从起始位置读取分区的记录 + */ + "auto.offset.reset" -> "latest", + /*是否自动提交*/ + "enable.auto.commit" -> (true: java.lang.Boolean) + ) + + /*可以同时订阅多个主题*/ + val topics = Array("spark-streaming-topic") + val stream = KafkaUtils.createDirectStream[String, String]( + streamingContext, + /*位置策略*/ + PreferConsistent, + /*订阅主题*/ + Subscribe[String, String](topics, kafkaParams) + ) + + /*打印输入流*/ + stream.map(record => (record.key, record.value)).print() + + streamingContext.start() + streamingContext.awaitTermination() + } +} +``` + +### 3.1 ConsumerRecord + +这里获得的输入流中每一个 Record 实际上是 `ConsumerRecord ` 的实例,其包含了 Record 的所有可用信息,源码如下: + +```scala +public class ConsumerRecord { + + public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP; + public static final int NULL_SIZE = -1; + public static final int NULL_CHECKSUM = -1; + + /*主题名称*/ + private final String topic; + /*分区编号*/ + private final int partition; + /*偏移量*/ + private final long offset; + /*时间戳*/ + private final long timestamp; + /*时间戳代表的含义*/ + private final TimestampType timestampType; + /*键序列化器*/ + private final int serializedKeySize; + /*值序列化器*/ + private final int serializedValueSize; + /*值序列化器*/ + private final Headers headers; + /*键*/ + private final K key; + /*值*/ + private final V value; + ..... +} +``` + +### 3.2 生产者属性 + +在示例代码中 `kafkaParams` 封装了 Kafka 消费者的属性,这些属性和 Spark Streaming 无关,是 Kafka 原生 API 中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下: + +#### 1. fetch.min.byte + +消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。 + +#### 2. fetch.max.wait.ms + +broker 返回给消费者数据的等待时间。 + +#### 3. max.partition.fetch.bytes + +分区返回给消费者的最大字节数。 + +#### 4. session.timeout.ms + +消费者在被认为死亡之前可以与服务器断开连接的时间。 + +#### 5. auto.offset.reset + +该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理: + +- latest(默认值) :在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据; +- earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。 + +#### 6. enable.auto.commit + +是否自动提交偏移量,默认值是 true,为了避免出现重复数据和数据丢失,可以把它设置为 false。 + +#### 7. client.id + +客户端 id,服务器用来识别消息的来源。 + +#### 8. max.poll.records + +单次调用 `poll()` 方法能够返回的记录数量。 + +#### 9. receive.buffer.bytes 和 send.buffer.byte + +这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。 + + + +### 3.3 位置策略 + +Spark Streaming 中提供了如下三种位置策略,用于指定 Kafka 主题分区与 Spark 执行程序 Executors 之间的分配关系: + ++ **PreferConsistent** : 它将在所有的 Executors 上均匀分配分区; + ++ **PreferBrokers** : 当 Spark 的 Executor 与 Kafka Broker 在同一机器上时可以选择该选项,它优先将该 Broker 上的首领分区分配给该机器上的 Executor; ++ **PreferFixed** : 可以指定主题分区与特定主机的映射关系,显示地将分区分配到特定的主机,其构造器如下: + +```scala +@Experimental +def PreferFixed(hostMap: collection.Map[TopicPartition, String]): LocationStrategy = + new PreferFixed(new ju.HashMap[TopicPartition, String](hostMap.asJava)) + +@Experimental +def PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy = + new PreferFixed(hostMap) +``` + + + +### 3.4 订阅方式 + +Spark Streaming 提供了两种主题订阅方式,分别为 `Subscribe` 和 `SubscribePattern`。后者可以使用正则匹配订阅主题的名称。其构造器分别如下: + +```scala +/** + * @param 需要订阅的主题的集合 + * @param Kafka 消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值 + */ +def Subscribe[K, V]( + topics: ju.Collection[jl.String], + kafkaParams: ju.Map[String, Object], + offsets: ju.Map[TopicPartition, jl.Long]): ConsumerStrategy[K, V] = { ... } + +/** + * @param 需要订阅的正则 + * @param Kafka 消费者参数 + * @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值 + */ +def SubscribePattern[K, V]( + pattern: ju.regex.Pattern, + kafkaParams: collection.Map[String, Object], + offsets: collection.Map[TopicPartition, Long]): ConsumerStrategy[K, V] = { ... } +``` + +在示例代码中,我们实际上并没有指定第三个参数 `offsets`,所以程序默认采用的是配置的 `auto.offset.reset` 属性的值 latest,即在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据。 + +### 3.5 提交偏移量 + +在示例代码中,我们将 `enable.auto.commit` 设置为 true,代表自动提交。在某些情况下,你可能需要更高的可靠性,如在业务完全处理完成后再提交偏移量,这时候可以使用手动提交。想要进行手动提交,需要调用 Kafka 原生的 API : + ++ `commitSync`: 用于异步提交; ++ `commitAsync`:用于同步提交。 + +具体提交方式可以参见:[Kafka 消费者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka 消费者详解.md) + + + +## 四、启动测试 + +### 4.1 创建主题 + +#### 1. 启动Kakfa + +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: + +```shell +# zookeeper启动命令 +bin/zkServer.sh start + +# 内置zookeeper启动命令 +bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动单节点 kafka 用于测试: + +```shell +# bin/kafka-server-start.sh config/server.properties +``` + +#### 2. 创建topic + +```shell +# 创建用于测试主题 +bin/kafka-topics.sh --create \ + --bootstrap-server hadoop001:9092 \ + --replication-factor 1 \ + --partitions 1 \ + --topic spark-streaming-topic + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +#### 3. 创建生产者 + +这里创建一个 Kafka 生产者,用于发送测试数据: + +```shell +bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streaming-topic +``` + +### 4.2 本地模式测试 + +这里我直接使用本地模式启动 Spark Streaming 程序。启动后使用生产者发送数据,从控制台查看结果。 + +从控制台输出中可以看到数据流已经被成功接收,由于采用 `kafka-console-producer.sh` 发送的数据默认是没有 key 的,所以 key 值为 null。同时从输出中也可以看到在程序中指定的 `groupId` 和程序自动分配的 `clientId`。 + +
+ + + + + +## 参考资料 + +1. https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Structured_API\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Structured_API\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" new file mode 100644 index 0000000..5ba02c4 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Structured_API\347\232\204\345\237\272\346\234\254\344\275\277\347\224\250.md" @@ -0,0 +1,244 @@ +# Structured API基本使用 + + + + +## 一、创建DataFrame和Dataset + +### 1.1 创建DataFrame + +Spark 中所有功能的入口点是 `SparkSession`,可以使用 `SparkSession.builder()` 创建。创建后应用程序就可以从现有 RDD,Hive 表或 Spark 数据源创建 DataFrame。示例如下: + +```scala +val spark = SparkSession.builder().appName("Spark-SQL").master("local[2]").getOrCreate() +val df = spark.read.json("/usr/file/json/emp.json") +df.show() + +// 建议在进行 spark SQL 编程前导入下面的隐式转换,因为 DataFrames 和 dataSets 中很多操作都依赖了隐式转换 +import spark.implicits._ +``` + +可以使用 `spark-shell` 进行测试,需要注意的是 `spark-shell` 启动后会自动创建一个名为 `spark` 的 `SparkSession`,在命令行中可以直接引用即可: + +
+ +
+ +### 1.2 创建Dataset + +Spark 支持由内部数据集和外部数据集来创建 DataSet,其创建方式分别如下: + +#### 1. 由外部数据集创建 + +```scala +// 1.需要导入隐式转换 +import spark.implicits._ + +// 2.创建 case class,等价于 Java Bean +case class Emp(ename: String, comm: Double, deptno: Long, empno: Long, + hiredate: String, job: String, mgr: Long, sal: Double) + +// 3.由外部数据集创建 Datasets +val ds = spark.read.json("/usr/file/emp.json").as[Emp] +ds.show() +``` + +#### 2. 由内部数据集创建 + +```scala +// 1.需要导入隐式转换 +import spark.implicits._ + +// 2.创建 case class,等价于 Java Bean +case class Emp(ename: String, comm: Double, deptno: Long, empno: Long, + hiredate: String, job: String, mgr: Long, sal: Double) + +// 3.由内部数据集创建 Datasets +val caseClassDS = Seq(Emp("ALLEN", 300.0, 30, 7499, "1981-02-20 00:00:00", "SALESMAN", 7698, 1600.0), + Emp("JONES", 300.0, 30, 7499, "1981-02-20 00:00:00", "SALESMAN", 7698, 1600.0)) + .toDS() +caseClassDS.show() +``` + +
+ +### 1.3 由RDD创建DataFrame + +Spark 支持两种方式把 RDD 转换为 DataFrame,分别是使用反射推断和指定 Schema 转换: + +#### 1. 使用反射推断 + +```scala +// 1.导入隐式转换 +import spark.implicits._ + +// 2.创建部门类 +case class Dept(deptno: Long, dname: String, loc: String) + +// 3.创建 RDD 并转换为 dataSet +val rddToDS = spark.sparkContext + .textFile("/usr/file/dept.txt") + .map(_.split("\t")) + .map(line => Dept(line(0).trim.toLong, line(1), line(2))) + .toDS() // 如果调用 toDF() 则转换为 dataFrame +``` + +#### 2. 以编程方式指定Schema + +```scala +import org.apache.spark.sql.Row +import org.apache.spark.sql.types._ + + +// 1.定义每个列的列类型 +val fields = Array(StructField("deptno", LongType, nullable = true), + StructField("dname", StringType, nullable = true), + StructField("loc", StringType, nullable = true)) + +// 2.创建 schema +val schema = StructType(fields) + +// 3.创建 RDD +val deptRDD = spark.sparkContext.textFile("/usr/file/dept.txt") +val rowRDD = deptRDD.map(_.split("\t")).map(line => Row(line(0).toLong, line(1), line(2))) + + +// 4.将 RDD 转换为 dataFrame +val deptDF = spark.createDataFrame(rowRDD, schema) +deptDF.show() +``` + +
+ +### 1.4 DataFrames与Datasets互相转换 + +Spark 提供了非常简单的转换方法用于 DataFrame 与 Dataset 间的互相转换,示例如下: + +```shell +# DataFrames转Datasets +scala> df.as[Emp] +res1: org.apache.spark.sql.Dataset[Emp] = [COMM: double, DEPTNO: bigint ... 6 more fields] + +# Datasets转DataFrames +scala> ds.toDF() +res2: org.apache.spark.sql.DataFrame = [COMM: double, DEPTNO: bigint ... 6 more fields] +``` + +
+ +## 二、Columns列操作 + +### 2.1 引用列 + +Spark 支持多种方法来构造和引用列,最简单的是使用 `col() ` 或 `column() ` 函数。 + +```scala +col("colName") +column("colName") + +// 对于 Scala 语言而言,还可以使用$"myColumn"和'myColumn 这两种语法糖进行引用。 +df.select($"ename", $"job").show() +df.select('ename, 'job).show() +``` + +### 2.2 新增列 + +```scala +// 基于已有列值新增列 +df.withColumn("upSal",$"sal"+1000) +// 基于固定值新增列 +df.withColumn("intCol",lit(1000)) +``` + +### 2.3 删除列 + +```scala +// 支持删除多个列 +df.drop("comm","job").show() +``` + +### 2.4 重命名列 + +```scala +df.withColumnRenamed("comm", "common").show() +``` + +需要说明的是新增,删除,重命名列都会产生新的 DataFrame,原来的 DataFrame 不会被改变。 + +
+ +## 三、使用Structured API进行基本查询 + +```scala +// 1.查询员工姓名及工作 +df.select($"ename", $"job").show() + +// 2.filter 查询工资大于 2000 的员工信息 +df.filter($"sal" > 2000).show() + +// 3.orderBy 按照部门编号降序,工资升序进行查询 +df.orderBy(desc("deptno"), asc("sal")).show() + +// 4.limit 查询工资最高的 3 名员工的信息 +df.orderBy(desc("sal")).limit(3).show() + +// 5.distinct 查询所有部门编号 +df.select("deptno").distinct().show() + +// 6.groupBy 分组统计部门人数 +df.groupBy("deptno").count().show() +``` + +
+ +## 四、使用Spark SQL进行基本查询 + +### 4.1 Spark SQL基本使用 + +```scala +// 1.首先需要将 DataFrame 注册为临时视图 +df.createOrReplaceTempView("emp") + +// 2.查询员工姓名及工作 +spark.sql("SELECT ename,job FROM emp").show() + +// 3.查询工资大于 2000 的员工信息 +spark.sql("SELECT * FROM emp where sal > 2000").show() + +// 4.orderBy 按照部门编号降序,工资升序进行查询 +spark.sql("SELECT * FROM emp ORDER BY deptno DESC,sal ASC").show() + +// 5.limit 查询工资最高的 3 名员工的信息 +spark.sql("SELECT * FROM emp ORDER BY sal DESC LIMIT 3").show() + +// 6.distinct 查询所有部门编号 +spark.sql("SELECT DISTINCT(deptno) FROM emp").show() + +// 7.分组统计部门人数 +spark.sql("SELECT deptno,count(ename) FROM emp group by deptno").show() +``` + +### 4.2 全局临时视图 + +上面使用 `createOrReplaceTempView` 创建的是会话临时视图,它的生命周期仅限于会话范围,会随会话的结束而结束。 + +你也可以使用 `createGlobalTempView` 创建全局临时视图,全局临时视图可以在所有会话之间共享,并直到整个 Spark 应用程序终止后才会消失。全局临时视图被定义在内置的 `global_temp` 数据库下,需要使用限定名称进行引用,如 `SELECT * FROM global_temp.view1`。 + +```scala +// 注册为全局临时视图 +df.createGlobalTempView("gemp") + +// 使用限定名称进行引用 +spark.sql("SELECT ename,job FROM global_temp.gemp").show() +``` + + + +## 参考资料 + +[Spark SQL, DataFrames and Datasets Guide > Getting Started](https://spark.apache.org/docs/latest/sql-getting-started.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Transformation\345\222\214Action\347\256\227\345\255\220.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Transformation\345\222\214Action\347\256\227\345\255\220.md" new file mode 100644 index 0000000..eb5603c --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark_Transformation\345\222\214Action\347\256\227\345\255\220.md" @@ -0,0 +1,418 @@ +# Transformation 和 Action 常用算子 + + + +## 一、Transformation + +spark 常用的 Transformation 算子如下表: + +| Transformation 算子 | Meaning(含义) | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| **map**(*func*) | 对原 RDD 中每个元素运用 *func* 函数,并生成新的 RDD | +| **filter**(*func*) | 对原 RDD 中每个元素使用*func* 函数进行过滤,并生成新的 RDD | +| **flatMap**(*func*) | 与 map 类似,但是每一个输入的 item 被映射成 0 个或多个输出的 items( *func* 返回类型需要为 Seq )。 | +| **mapPartitions**(*func*) | 与 map 类似,但函数单独在 RDD 的每个分区上运行, *func*函数的类型为 Iterator\ => Iterator\ ,其中 T 是 RDD 的类型,即 RDD[T] | +| **mapPartitionsWithIndex**(*func*) | 与 mapPartitions 类似,但 *func* 类型为 (Int, Iterator\) => Iterator\ ,其中第一个参数为分区索引 | +| **sample**(*withReplacement*, *fraction*, *seed*) | 数据采样,有三个可选参数:设置是否放回(withReplacement)、采样的百分比(*fraction*)、随机数生成器的种子(seed); | +| **union**(*otherDataset*) | 合并两个 RDD | +| **intersection**(*otherDataset*) | 求两个 RDD 的交集 | +| **distinct**([*numTasks*])) | 去重 | +| **groupByKey**([*numTasks*]) | 按照 key 值进行分区,即在一个 (K, V) 对的 dataset 上调用时,返回一个 (K, Iterable\)
**Note:** 如果分组是为了在每一个 key 上执行聚合操作(例如,sum 或 average),此时使用 `reduceByKey` 或 `aggregateByKey` 性能会更好
**Note:** 默认情况下,并行度取决于父 RDD 的分区数。可以传入 `numTasks` 参数进行修改。 | +| **reduceByKey**(*func*, [*numTasks*]) | 按照 key 值进行分组,并对分组后的数据执行归约操作。 | +| **aggregateByKey**(*zeroValue*,*numPartitions*)(*seqOp*, *combOp*, [*numTasks*]) | 当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 groupByKey 类似,reduce 任务的数量可通过第二个参数进行配置。 | +| **sortByKey**([*ascending*], [*numTasks*]) | 按照 key 进行排序,其中的 key 需要实现 Ordered 特质,即可比较 | +| **join**(*otherDataset*, [*numTasks*]) | 在一个 (K, V) 和 (K, W) 类型的 dataset 上调用时,返回一个 (K, (V, W)) pairs 的 dataset,等价于内连接操作。如果想要执行外连接,可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 | +| **cogroup**(*otherDataset*, [*numTasks*]) | 在一个 (K, V) 对的 dataset 上调用时,返回一个 (K, (Iterable\, Iterable\)) tuples 的 dataset。 | +| **cartesian**(*otherDataset*) | 在一个 T 和 U 类型的 dataset 上调用时,返回一个 (T, U) 类型的 dataset(即笛卡尔积)。 | +| **coalesce**(*numPartitions*) | 将 RDD 中的分区数减少为 numPartitions。 | +| **repartition**(*numPartitions*) | 随机重新调整 RDD 中的数据以创建更多或更少的分区,并在它们之间进行平衡。 | +| **repartitionAndSortWithinPartitions**(*partitioner*) | 根据给定的 partitioner(分区器)对 RDD 进行重新分区,并对分区中的数据按照 key 值进行排序。这比调用 `repartition` 然后再 sorting(排序)效率更高,因为它可以将排序过程推送到 shuffle 操作所在的机器。 | + +下面分别给出这些算子的基本使用示例: + +### 1.1 map + +```scala +val list = List(1,2,3) +sc.parallelize(list).map(_ * 10).foreach(println) + +// 输出结果: 10 20 30 (这里为了节省篇幅去掉了换行,后文亦同) +``` + +### 1.2 filter + +```scala +val list = List(3, 6, 9, 10, 12, 21) +sc.parallelize(list).filter(_ >= 10).foreach(println) + +// 输出: 10 12 21 +``` + +### 1.3 flatMap + +`flatMap(func)` 与 `map` 类似,但每一个输入的 item 会被映射成 0 个或多个输出的 items( *func* 返回类型需要为 `Seq`)。 + +```scala +val list = List(List(1, 2), List(3), List(), List(4, 5)) +sc.parallelize(list).flatMap(_.toList).map(_ * 10).foreach(println) + +// 输出结果 : 10 20 30 40 50 +``` + +flatMap 这个算子在日志分析中使用概率非常高,这里进行一下演示:拆分输入的每行数据为单个单词,并赋值为 1,代表出现一次,之后按照单词分组并统计其出现总次数,代码如下: + +```scala +val lines = List("spark flume spark", + "hadoop flume hive") +sc.parallelize(lines).flatMap(line => line.split(" ")). +map(word=>(word,1)).reduceByKey(_+_).foreach(println) + +// 输出: +(spark,2) +(hive,1) +(hadoop,1) +(flume,2) +``` + +### 1.4 mapPartitions + +与 map 类似,但函数单独在 RDD 的每个分区上运行, *func*函数的类型为 `Iterator => Iterator` (其中 T 是 RDD 的类型),即输入和输出都必须是可迭代类型。 + +```scala +val list = List(1, 2, 3, 4, 5, 6) +sc.parallelize(list, 3).mapPartitions(iterator => { + val buffer = new ListBuffer[Int] + while (iterator.hasNext) { + buffer.append(iterator.next() * 100) + } + buffer.toIterator +}).foreach(println) +//输出结果 +100 200 300 400 500 600 +``` + +### 1.5 mapPartitionsWithIndex + + 与 mapPartitions 类似,但 *func* 类型为 `(Int, Iterator) => Iterator` ,其中第一个参数为分区索引。 + +```scala +val list = List(1, 2, 3, 4, 5, 6) +sc.parallelize(list, 3).mapPartitionsWithIndex((index, iterator) => { + val buffer = new ListBuffer[String] + while (iterator.hasNext) { + buffer.append(index + "分区:" + iterator.next() * 100) + } + buffer.toIterator +}).foreach(println) +//输出 +0 分区:100 +0 分区:200 +1 分区:300 +1 分区:400 +2 分区:500 +2 分区:600 +``` + +### 1.6 sample + + 数据采样。有三个可选参数:设置是否放回 (withReplacement)、采样的百分比 (fraction)、随机数生成器的种子 (seed) : + +```scala +val list = List(1, 2, 3, 4, 5, 6) +sc.parallelize(list).sample(withReplacement = false, fraction = 0.5).foreach(println) +``` + +### 1.7 union + +合并两个 RDD: + +```scala +val list1 = List(1, 2, 3) +val list2 = List(4, 5, 6) +sc.parallelize(list1).union(sc.parallelize(list2)).foreach(println) +// 输出: 1 2 3 4 5 6 +``` + +### 1.8 intersection + +求两个 RDD 的交集: + +```scala +val list1 = List(1, 2, 3, 4, 5) +val list2 = List(4, 5, 6) +sc.parallelize(list1).intersection(sc.parallelize(list2)).foreach(println) +// 输出: 4 5 +``` + +### 1.9 distinct + +去重: + +```scala +val list = List(1, 2, 2, 4, 4) +sc.parallelize(list).distinct().foreach(println) +// 输出: 4 1 2 +``` + +### 1.10 groupByKey + +按照键进行分组: + +```scala +val list = List(("hadoop", 2), ("spark", 3), ("spark", 5), ("storm", 6), ("hadoop", 2)) +sc.parallelize(list).groupByKey().map(x => (x._1, x._2.toList)).foreach(println) + +//输出: +(spark,List(3, 5)) +(hadoop,List(2, 2)) +(storm,List(6)) +``` + +### 1.11 reduceByKey + +按照键进行归约操作: + +```scala +val list = List(("hadoop", 2), ("spark", 3), ("spark", 5), ("storm", 6), ("hadoop", 2)) +sc.parallelize(list).reduceByKey(_ + _).foreach(println) + +//输出 +(spark,8) +(hadoop,4) +(storm,6) +``` + +### 1.12 sortBy & sortByKey + +按照键进行排序: + +```scala +val list01 = List((100, "hadoop"), (90, "spark"), (120, "storm")) +sc.parallelize(list01).sortByKey(ascending = false).foreach(println) +// 输出 +(120,storm) +(90,spark) +(100,hadoop) +``` + +按照指定元素进行排序: + +```scala +val list02 = List(("hadoop",100), ("spark",90), ("storm",120)) +sc.parallelize(list02).sortBy(x=>x._2,ascending=false).foreach(println) +// 输出 +(storm,120) +(hadoop,100) +(spark,90) +``` + +### 1.13 join + +在一个 (K, V) 和 (K, W) 类型的 Dataset 上调用时,返回一个 (K, (V, W)) 的 Dataset,等价于内连接操作。如果想要执行外连接,可以使用 `leftOuterJoin`, `rightOuterJoin` 和 `fullOuterJoin` 等算子。 + +```scala +val list01 = List((1, "student01"), (2, "student02"), (3, "student03")) +val list02 = List((1, "teacher01"), (2, "teacher02"), (3, "teacher03")) +sc.parallelize(list01).join(sc.parallelize(list02)).foreach(println) + +// 输出 +(1,(student01,teacher01)) +(3,(student03,teacher03)) +(2,(student02,teacher02)) +``` + +### 1.14 cogroup + +在一个 (K, V) 对的 Dataset 上调用时,返回多个类型为 (K, (Iterable\, Iterable\)) 的元组所组成的 Dataset。 + +```scala +val list01 = List((1, "a"),(1, "a"), (2, "b"), (3, "e")) +val list02 = List((1, "A"), (2, "B"), (3, "E")) +val list03 = List((1, "[ab]"), (2, "[bB]"), (3, "eE"),(3, "eE")) +sc.parallelize(list01).cogroup(sc.parallelize(list02),sc.parallelize(list03)).foreach(println) + +// 输出: 同一个 RDD 中的元素先按照 key 进行分组,然后再对不同 RDD 中的元素按照 key 进行分组 +(1,(CompactBuffer(a, a),CompactBuffer(A),CompactBuffer([ab]))) +(3,(CompactBuffer(e),CompactBuffer(E),CompactBuffer(eE, eE))) +(2,(CompactBuffer(b),CompactBuffer(B),CompactBuffer([bB]))) + +``` + +### 1.15 cartesian + +计算笛卡尔积: + +```scala +val list1 = List("A", "B", "C") +val list2 = List(1, 2, 3) +sc.parallelize(list1).cartesian(sc.parallelize(list2)).foreach(println) + +//输出笛卡尔积 +(A,1) +(A,2) +(A,3) +(B,1) +(B,2) +(B,3) +(C,1) +(C,2) +(C,3) +``` + +### 1.16 aggregateByKey + +当调用(K,V)对的数据集时,返回(K,U)对的数据集,其中使用给定的组合函数和 zeroValue 聚合每个键的值。与 `groupByKey` 类似,reduce 任务的数量可通过第二个参数 `numPartitions` 进行配置。示例如下: + +```scala +// 为了清晰,以下所有参数均使用具名传参 +val list = List(("hadoop", 3), ("hadoop", 2), ("spark", 4), ("spark", 3), ("storm", 6), ("storm", 8)) +sc.parallelize(list,numSlices = 2).aggregateByKey(zeroValue = 0,numPartitions = 3)( + seqOp = math.max(_, _), + combOp = _ + _ + ).collect.foreach(println) +//输出结果: +(hadoop,3) +(storm,8) +(spark,7) +``` + +这里使用了 `numSlices = 2` 指定 aggregateByKey 父操作 parallelize 的分区数量为 2,其执行流程如下: + +
+ +基于同样的执行流程,如果 `numSlices = 1`,则意味着只有输入一个分区,则其最后一步 combOp 相当于是无效的,执行结果为: + +```properties +(hadoop,3) +(storm,8) +(spark,4) +``` + +同样的,如果每个单词对一个分区,即 `numSlices = 6`,此时相当于求和操作,执行结果为: + +```properties +(hadoop,5) +(storm,14) +(spark,7) +``` + +`aggregateByKey(zeroValue = 0,numPartitions = 3)` 的第二个参数 `numPartitions` 决定的是输出 RDD 的分区数量,想要验证这个问题,可以对上面代码进行改写,使用 `getNumPartitions` 方法获取分区数量: + +```scala +sc.parallelize(list,numSlices = 6).aggregateByKey(zeroValue = 0,numPartitions = 3)( + seqOp = math.max(_, _), + combOp = _ + _ +).getNumPartitions +``` + +
+ +## 二、Action + +Spark 常用的 Action 算子如下: + +| Action(动作) | Meaning(含义) | +| -------------------------------------------------- | ------------------------------------------------------------ | +| **reduce**(*func*) | 使用函数*func*执行归约操作 | +| **collect**() | 以一个 array 数组的形式返回 dataset 的所有元素,适用于小结果集。 | +| **count**() | 返回 dataset 中元素的个数。 | +| **first**() | 返回 dataset 中的第一个元素,等价于 take(1)。 | +| **take**(*n*) | 将数据集中的前 *n* 个元素作为一个 array 数组返回。 | +| **takeSample**(*withReplacement*, *num*, [*seed*]) | 对一个 dataset 进行随机抽样 | +| **takeOrdered**(*n*, *[ordering]*) | 按自然顺序(natural order)或自定义比较器(custom comparator)排序后返回前 *n* 个元素。只适用于小结果集,因为所有数据都会被加载到驱动程序的内存中进行排序。 | +| **saveAsTextFile**(*path*) | 将 dataset 中的元素以文本文件的形式写入本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。Spark 将对每个元素调用 toString 方法,将元素转换为文本文件中的一行记录。 | +| **saveAsSequenceFile**(*path*) | 将 dataset 中的元素以 Hadoop SequenceFile 的形式写入到本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。该操作要求 RDD 中的元素需要实现 Hadoop 的 Writable 接口。对于 Scala 语言而言,它可以将 Spark 中的基本数据类型自动隐式转换为对应 Writable 类型。(目前仅支持 Java and Scala) | +| **saveAsObjectFile**(*path*) | 使用 Java 序列化后存储,可以使用 `SparkContext.objectFile()` 进行加载。(目前仅支持 Java and Scala) | +| **countByKey**() | 计算每个键出现的次数。 | +| **foreach**(*func*) | 遍历 RDD 中每个元素,并对其执行*fun*函数 | + +### 2.1 reduce + +使用函数*func*执行归约操作: + +```scala + val list = List(1, 2, 3, 4, 5) +sc.parallelize(list).reduce((x, y) => x + y) +sc.parallelize(list).reduce(_ + _) + +// 输出 15 +``` + +### 2.2 takeOrdered + +按自然顺序(natural order)或自定义比较器(custom comparator)排序后返回前 *n* 个元素。需要注意的是 `takeOrdered` 使用隐式参数进行隐式转换,以下为其源码。所以在使用自定义排序时,需要继承 `Ordering[T]` 实现自定义比较器,然后将其作为隐式参数引入。 + +```scala +def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope { + ......... +} +``` + +自定义规则排序: + +```scala +// 继承 Ordering[T],实现自定义比较器,按照 value 值的长度进行排序 +class CustomOrdering extends Ordering[(Int, String)] { + override def compare(x: (Int, String), y: (Int, String)): Int + = if (x._2.length > y._2.length) 1 else -1 +} + +val list = List((1, "hadoop"), (1, "storm"), (1, "azkaban"), (1, "hive")) +// 引入隐式默认值 +implicit val implicitOrdering = new CustomOrdering +sc.parallelize(list).takeOrdered(5) + +// 输出: Array((1,hive), (1,storm), (1,hadoop), (1,azkaban) +``` + +### 2.3 countByKey + +计算每个键出现的次数: + +```scala +val list = List(("hadoop", 10), ("hadoop", 10), ("storm", 3), ("storm", 3), ("azkaban", 1)) +sc.parallelize(list).countByKey() + +// 输出: Map(hadoop -> 2, storm -> 2, azkaban -> 1) +``` + +### 2.4 saveAsTextFile + +将 dataset 中的元素以文本文件的形式写入本地文件系统、HDFS 或其它 Hadoop 支持的文件系统中。Spark 将对每个元素调用 toString 方法,将元素转换为文本文件中的一行记录。 + +```scala +val list = List(("hadoop", 10), ("hadoop", 10), ("storm", 3), ("storm", 3), ("azkaban", 1)) +sc.parallelize(list).saveAsTextFile("/usr/file/temp") +``` + + + + + +## 参考资料 + +[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\256\200\344\273\213.md" new file mode 100644 index 0000000..b81a381 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\256\200\344\273\213.md" @@ -0,0 +1,94 @@ +# Spark简介 + + + +## 一、简介 + +Spark 于 2009 年诞生于加州大学伯克利分校 AMPLab,2013 年被捐赠给 Apache 软件基金会,2014 年 2 月成为 Apache 的顶级项目。相对于 MapReduce 的批处理计算,Spark 可以带来上百倍的性能提升,因此它成为继 MapReduce 之后,最为广泛使用的分布式计算框架。 + +## 二、特点 + +Apache Spark 具有以下特点: + ++ 使用先进的 DAG 调度程序,查询优化器和物理执行引擎,以实现性能上的保证; ++ 多语言支持,目前支持的有 Java,Scala,Python 和 R; ++ 提供了 80 多个高级 API,可以轻松地构建应用程序; ++ 支持批处理,流处理和复杂的业务分析; ++ 丰富的类库支持:包括 SQL,MLlib,GraphX 和 Spark Streaming 等库,并且可以将它们无缝地进行组合; ++ 丰富的部署模式:支持本地模式和自带的集群模式,也支持在 Hadoop,Mesos,Kubernetes 上运行; ++ 多数据源支持:支持访问 HDFS,Alluxio,Cassandra,HBase,Hive 以及数百个其他数据源中的数据。 + +
+ +## 三、集群架构 + +| Term(术语) | Meaning(含义) | +| --------------- | ------------------------------------------------------------ | +| Application | Spark 应用程序,由集群上的一个 Driver 节点和多个 Executor 节点组成。 | +| Driver program | 主运用程序,该进程运行应用的 main() 方法并且创建 SparkContext | +| Cluster manager | 集群资源管理器(例如,Standlone Manager,Mesos,YARN) | +| Worker node | 执行计算任务的工作节点 | +| Executor | 位于工作节点上的应用进程,负责执行计算任务并且将输出数据保存到内存或者磁盘中 | +| Task | 被发送到 Executor 中的工作单元 | + +
+ +**执行过程**: + +1. 用户程序创建 SparkContext 后,它会连接到集群资源管理器,集群资源管理器会为用户程序分配计算资源,并启动 Executor; +2. Dirver 将计算程序划分为不同的执行阶段和多个 Task,之后将 Task 发送给 Executor; +3. Executor 负责执行 Task,并将执行状态汇报给 Driver,同时也会将当前节点资源的使用情况汇报给集群资源管理器。 + +## 四、核心组件 + +Spark 基于 Spark Core 扩展了四个核心组件,分别用于满足不同领域的计算需求。 + +
+ +### 3.1 Spark SQL + +Spark SQL 主要用于结构化数据的处理。其具有以下特点: + +- 能够将 SQL 查询与 Spark 程序无缝混合,允许您使用 SQL 或 DataFrame API 对结构化数据进行查询; +- 支持多种数据源,包括 Hive,Avro,Parquet,ORC,JSON 和 JDBC; +- 支持 HiveQL 语法以及用户自定义函数 (UDF),允许你访问现有的 Hive 仓库; +- 支持标准的 JDBC 和 ODBC 连接; +- 支持优化器,列式存储和代码生成等特性,以提高查询效率。 + +### 3.2 Spark Streaming + +Spark Streaming 主要用于快速构建可扩展,高吞吐量,高容错的流处理程序。支持从 HDFS,Flume,Kafka,Twitter 和 ZeroMQ 读取数据,并进行处理。 + +
+ + Spark Streaming 的本质是微批处理,它将数据流进行极小粒度的拆分,拆分为多个批处理,从而达到接近于流处理的效果。 + +
+ + + +### 3.3 MLlib + +MLlib 是 Spark 的机器学习库。其设计目标是使得机器学习变得简单且可扩展。它提供了以下工具: + ++ **常见的机器学习算法**:如分类,回归,聚类和协同过滤; ++ **特征化**:特征提取,转换,降维和选择; ++ **管道**:用于构建,评估和调整 ML 管道的工具; ++ **持久性**:保存和加载算法,模型,管道数据; ++ **实用工具**:线性代数,统计,数据处理等。 + +### 3.4 Graphx + +GraphX 是 Spark 中用于图形计算和图形并行计算的新组件。在高层次上,GraphX 通过引入一个新的图形抽象来扩展 RDD(一种具有附加到每个顶点和边缘的属性的定向多重图形)。为了支持图计算,GraphX 提供了一组基本运算符(如: subgraph,joinVertices 和 aggregateMessages)以及优化后的 Pregel API。此外,GraphX 还包括越来越多的图形算法和构建器,以简化图形分析任务。 + +## diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\264\257\345\212\240\345\231\250\344\270\216\345\271\277\346\222\255\345\217\230\351\207\217.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\264\257\345\212\240\345\231\250\344\270\216\345\271\277\346\222\255\345\217\230\351\207\217.md" new file mode 100644 index 0000000..3af99a6 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\347\264\257\345\212\240\345\231\250\344\270\216\345\271\277\346\222\255\345\217\230\351\207\217.md" @@ -0,0 +1,105 @@ +# Spark 累加器与广播变量 + + + +## 一、简介 + +在 Spark 中,提供了两种类型的共享变量:累加器 (accumulator) 与广播变量 (broadcast variable): + ++ **累加器**:用来对信息进行聚合,主要用于累计计数等场景; ++ **广播变量**:主要用于在节点间高效分发大对象。 + +## 二、累加器 + +这里先看一个具体的场景,对于正常的累计求和,如果在集群模式中使用下面的代码进行计算,会发现执行结果并非预期: + +```scala +var counter = 0 +val data = Array(1, 2, 3, 4, 5) +sc.parallelize(data).foreach(x => counter += x) + println(counter) +``` + +counter 最后的结果是 0,导致这个问题的主要原因是闭包。 + +
+ + + +### 2.1 理解闭包 + +**1. Scala 中闭包的概念** + +这里先介绍一下 Scala 中关于闭包的概念: + +``` +var more = 10 +val addMore = (x: Int) => x + more +``` + +如上函数 `addMore` 中有两个变量 x 和 more: + +- **x** : 是一个绑定变量 (bound variable),因为其是该函数的入参,在函数的上下文中有明确的定义; +- **more** : 是一个自由变量 (free variable),因为函数字面量本生并没有给 more 赋予任何含义。 + +按照定义:在创建函数时,如果需要捕获自由变量,那么包含指向被捕获变量的引用的函数就被称为闭包函数。 + +**2. Spark 中的闭包** + +在实际计算时,Spark 会将对 RDD 操作分解为 Task,Task 运行在 Worker Node 上。在执行之前,Spark 会对任务进行闭包,如果闭包内涉及到自由变量,则程序会进行拷贝,并将副本变量放在闭包中,之后闭包被序列化并发送给每个执行者。因此,当在 foreach 函数中引用 `counter` 时,它将不再是 Driver 节点上的 `counter`,而是闭包中的副本 `counter`,默认情况下,副本 `counter` 更新后的值不会回传到 Driver,所以 `counter` 的最终值仍然为零。 + +需要注意的是:在 Local 模式下,有可能执行 `foreach` 的 Worker Node 与 Diver 处在相同的 JVM,并引用相同的原始 `counter`,这时候更新可能是正确的,但是在集群模式下一定不正确。所以在遇到此类问题时应优先使用累加器。 + +累加器的原理实际上很简单:就是将每个副本变量的最终值传回 Driver,由 Driver 聚合后得到最终值,并更新原始变量。 + + +
+ +### 2.2 使用累加器 + +`SparkContext` 中定义了所有创建累加器的方法,需要注意的是:被中横线划掉的累加器方法在 Spark 2.0.0 之后被标识为废弃。 + +
+ +使用示例和执行结果分别如下: + +```scala +val data = Array(1, 2, 3, 4, 5) +// 定义累加器 +val accum = sc.longAccumulator("My Accumulator") +sc.parallelize(data).foreach(x => accum.add(x)) +// 获取累加器的值 +accum.value +``` + +
+ + + +## 三、广播变量 + +在上面介绍中闭包的过程中我们说道每个 Task 任务的闭包都会持有自由变量的副本,如果变量很大且 Task 任务很多的情况下,这必然会对网络 IO 造成压力,为了解决这个情况,Spark 提供了广播变量。 + +广播变量的做法很简单:就是不把副本变量分发到每个 Task 中,而是将其分发到每个 Executor,Executor 中的所有 Task 共享一个副本变量。 + +```scala +// 把一个数组定义为一个广播变量 +val broadcastVar = sc.broadcast(Array(1, 2, 3, 4, 5)) +// 之后用到该数组时应优先使用广播变量,而不是原值 +sc.parallelize(broadcastVar.value).map(_ * 10).collect() +``` + + + + + +## 参考资料 + +[RDD Programming Guide](http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-programming-guide) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\351\203\250\347\275\262\346\250\241\345\274\217\344\270\216\344\275\234\344\270\232\346\217\220\344\272\244.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\351\203\250\347\275\262\346\250\241\345\274\217\344\270\216\344\275\234\344\270\232\346\217\220\344\272\244.md" new file mode 100644 index 0000000..229002c --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spark\351\203\250\347\275\262\346\250\241\345\274\217\344\270\216\344\275\234\344\270\232\346\217\220\344\272\244.md" @@ -0,0 +1,248 @@ +# Spark部署模式与作业提交 + + + + +## 一、作业提交 + +### 1.1 spark-submit + +Spark 所有模式均使用 `spark-submit` 命令提交作业,其格式如下: + +```shell +./bin/spark-submit \ + --class \ # 应用程序主入口类 + --master \ # 集群的 Master Url + --deploy-mode \ # 部署模式 + --conf = \ # 可选配置 + ... # other options + \ # Jar 包路径 + [application-arguments] #传递给主入口类的参数 +``` + +需要注意的是:在集群环境下,`application-jar` 必须能被集群中所有节点都能访问,可以是 HDFS 上的路径;也可以是本地文件系统路径,如果是本地文件系统路径,则要求集群中每一个机器节点上的相同路径都存在该 Jar 包。 + +### 1.2 deploy-mode + +deploy-mode 有 `cluster` 和 `client` 两个可选参数,默认为 `client`。这里以 Spark On Yarn 模式对两者进行说明 : + ++ 在 cluster 模式下,Spark Drvier 在应用程序的 Master 进程内运行,该进程由群集上的 YARN 管理,提交作业的客户端可以在启动应用程序后关闭; ++ 在 client 模式下,Spark Drvier 在提交作业的客户端进程中运行,Master 进程仅用于从 YARN 请求资源。 + +### 1.3 master-url + +master-url 的所有可选参数如下表所示: + +| Master URL | Meaning | +| --------------------------------- | ------------------------------------------------------------ | +| `local` | 使用一个线程本地运行 Spark | +| `local[K]` | 使用 K 个 worker 线程本地运行 Spark | +| `local[K,F]` | 使用 K 个 worker 线程本地运行 , 第二个参数为 Task 的失败重试次数 | +| `local[*]` | 使用与 CPU 核心数一样的线程数在本地运行 Spark | +| `local[*,F]` | 使用与 CPU 核心数一样的线程数在本地运行 Spark
第二个参数为 Task 的失败重试次数 | +| `spark://HOST:PORT` | 连接至指定的 standalone 集群的 master 节点。端口号默认是 7077。 | +| `spark://HOST1:PORT1,HOST2:PORT2` | 如果 standalone 集群采用 Zookeeper 实现高可用,则必须包含由 zookeeper 设置的所有 master 主机地址。 | +| `mesos://HOST:PORT` | 连接至给定的 Mesos 集群。端口默认是 5050。对于使用了 ZooKeeper 的 Mesos cluster 来说,使用 `mesos://zk://...` 来指定地址,使用 `--deploy-mode cluster` 模式来提交。 | +| `yarn` | 连接至一个 YARN 集群,集群由配置的 `HADOOP_CONF_DIR` 或者 `YARN_CONF_DIR` 来决定。使用 `--deploy-mode` 参数来配置 `client` 或 `cluster` 模式。 | + +下面主要介绍三种常用部署模式及对应的作业提交方式。 + +## 二、Local模式 + +Local 模式下提交作业最为简单,不需要进行任何配置,提交命令如下: + +```shell +# 本地模式提交应用 +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master local[2] \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 # 传给 SparkPi 的参数 +``` + +`spark-examples_2.11-2.4.0.jar` 是 Spark 提供的测试用例包,`SparkPi` 用于计算 Pi 值,执行结果如下: + +
+ + + +## 三、Standalone模式 + +Standalone 是 Spark 提供的一种内置的集群模式,采用内置的资源管理器进行管理。下面按照如图所示演示 1 个 Mater 和 2 个 Worker 节点的集群配置,这里使用两台主机进行演示: + ++ hadoop001: 由于只有两台主机,所以 hadoop001 既是 Master 节点,也是 Worker 节点; ++ hadoop002 : Worker 节点。 + + + + + +
+ +### 3.1 环境配置 + +首先需要保证 Spark 已经解压在两台主机的相同路径上。然后进入 hadoop001 的 `${SPARK_HOME}/conf/` 目录下,拷贝配置样本并进行相关配置: + +```shell +# cp spark-env.sh.template spark-env.sh +``` + +在 `spark-env.sh` 中配置 JDK 的目录,完成后将该配置使用 scp 命令分发到 hadoop002 上: + +```shell +# JDK安装位置 +JAVA_HOME=/usr/java/jdk1.8.0_201 +``` + +### 3.2 集群配置 + +在 `${SPARK_HOME}/conf/` 目录下,拷贝集群配置样本并进行相关配置: + +``` +# cp slaves.template slaves +``` + +指定所有 Worker 节点的主机名: + +```shell +# A Spark Worker will be started on each of the machines listed below. +hadoop001 +hadoop002 +``` + +这里需要注意以下三点: + ++ 主机名与 IP 地址的映射必须在 `/etc/hosts` 文件中已经配置,否则就直接使用 IP 地址; ++ 每个主机名必须独占一行; ++ Spark 的 Master 主机是通过 SSH 访问所有的 Worker 节点,所以需要预先配置免密登录。 + +### 3.3 启动 + +使用 `start-all.sh` 代表启动 Master 和所有 Worker 服务。 + +```shell +./sbin/start-master.sh +``` + +访问 8080 端口,查看 Spark 的 Web-UI 界面,,此时应该显示有两个有效的工作节点: + +
+ +### 3.4 提交作业 + +```shell +# 以client模式提交到standalone集群 +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master spark://hadoop001:7077 \ +--executor-memory 2G \ +--total-executor-cores 10 \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 + +# 以cluster模式提交到standalone集群 +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master spark://207.184.161.138:7077 \ +--deploy-mode cluster \ +--supervise \ # 配置此参数代表开启监督,如果主应用程序异常退出,则自动重启 Driver +--executor-memory 2G \ +--total-executor-cores 10 \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 +``` + +### 3.5 可选配置 + +在虚拟机上提交作业时经常出现一个的问题是作业无法申请到足够的资源: + +```properties +Initial job has not accepted any resources; +check your cluster UI to ensure that workers are registered and have sufficient resources +``` + +
+ +
+ +这时候可以查看 Web UI,我这里是内存空间不足:提交命令中要求作业的 `executor-memory` 是 2G,但是实际的工作节点的 `Memory` 只有 1G,这时候你可以修改 `--executor-memory`,也可以修改 Woker 的 `Memory`,其默认值为主机所有可用内存值减去 1G。 + +
+ +
+ +关于 Master 和 Woker 节点的所有可选配置如下,可以在 `spark-env.sh` 中进行对应的配置: + +| Environment Variable(环境变量) | Meaning(含义) | +| -------------------------------- | ------------------------------------------------------------ | +| `SPARK_MASTER_HOST` | master 节点地址 | +| `SPARK_MASTER_PORT` | master 节点地址端口(默认:7077) | +| `SPARK_MASTER_WEBUI_PORT` | master 的 web UI 的端口(默认:8080) | +| `SPARK_MASTER_OPTS` | 仅用于 master 的配置属性,格式是 "-Dx=y"(默认:none),所有属性可以参考官方文档:[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) | +| `SPARK_LOCAL_DIRS` | spark 的临时存储的目录,用于暂存 map 的输出和持久化存储 RDDs。多个目录用逗号分隔 | +| `SPARK_WORKER_CORES` | spark worker 节点可以使用 CPU Cores 的数量。(默认:全部可用) | +| `SPARK_WORKER_MEMORY` | spark worker 节点可以使用的内存数量(默认:全部的内存减去 1GB); | +| `SPARK_WORKER_PORT` | spark worker 节点的端口(默认: random(随机)) | +| `SPARK_WORKER_WEBUI_PORT` | worker 的 web UI 的 Port(端口)(默认:8081) | +| `SPARK_WORKER_DIR` | worker 运行应用程序的目录,这个目录中包含日志和暂存空间(default:SPARK_HOME/work) | +| `SPARK_WORKER_OPTS` | 仅用于 worker 的配置属性,格式是 "-Dx=y"(默认:none)。所有属性可以参考官方文档:[spark-standalone-mode](https://spark.apache.org/docs/latest/spark-standalone.html#spark-standalone-mode) | +| `SPARK_DAEMON_MEMORY` | 分配给 spark master 和 worker 守护进程的内存。(默认: 1G) | +| `SPARK_DAEMON_JAVA_OPTS` | spark master 和 worker 守护进程的 JVM 选项,格式是 "-Dx=y"(默认:none) | +| `SPARK_PUBLIC_DNS` | spark master 和 worker 的公开 DNS 名称。(默认:none) | + + + +## 三、Spark on Yarn模式 + +Spark 支持将作业提交到 Yarn 上运行,此时不需要启动 Master 节点,也不需要启动 Worker 节点。 + +### 3.1 配置 + +在 `spark-env.sh` 中配置 hadoop 的配置目录的位置,可以使用 `YARN_CONF_DIR` 或 `HADOOP_CONF_DIR` 进行指定: + +```properties +YARN_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop +# JDK安装位置 +JAVA_HOME=/usr/java/jdk1.8.0_201 +``` + +### 3.2 启动 + +必须要保证 Hadoop 已经启动,这里包括 YARN 和 HDFS 都需要启动,因为在计算过程中 Spark 会使用 HDFS 存储临时文件,如果 HDFS 没有启动,则会抛出异常。 + +```shell +# start-yarn.sh +# start-dfs.sh +``` + +### 3.3 提交应用 + +```shell +# 以client模式提交到yarn集群 +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master yarn \ +--deploy-mode client \ +--executor-memory 2G \ +--num-executors 10 \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 + +# 以cluster模式提交到yarn集群 +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master yarn \ +--deploy-mode cluster \ +--executor-memory 2G \ +--num-executors 10 \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 +``` + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spring+Mybtais+Phoenix\346\225\264\345\220\210.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spring+Mybtais+Phoenix\346\225\264\345\220\210.md" new file mode 100644 index 0000000..68827be --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Spring+Mybtais+Phoenix\346\225\264\345\220\210.md" @@ -0,0 +1,386 @@ +# Spring/Spring Boot 整合 Mybatis + Phoenix + + + +## 一、前言 + +使用 Spring+Mybatis 操作 Phoenix 和操作其他的关系型数据库(如 Mysql,Oracle)在配置上是基本相同的,下面会分别给出 Spring/Spring Boot 整合步骤,完整代码见本仓库: + ++ [Spring + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/tree/master/code/Phoenix/spring-mybatis-phoenix) ++ [SpringBoot + Mybatis + Phoenix](https://github.com/heibaiying/BigData-Notes/tree/master/code/Phoenix/spring-boot-mybatis-phoenix) + +## 二、Spring + Mybatis + Phoenix + +### 2.1 项目结构 + +
+ +### 2.2 主要依赖 + +除了 Spring 相关依赖外,还需要导入 `phoenix-core` 和对应的 Mybatis 依赖包 + +```xml + + + org.mybatis + mybatis-spring + 1.3.2 + + + org.mybatis + mybatis + 3.4.6 + + + + org.apache.phoenix + phoenix-core + 4.14.0-cdh5.14.2 + +``` + +### 2.3 数据库配置文件 + +在数据库配置文件 `jdbc.properties` 中配置数据库驱动和 zookeeper 地址 + +```properties +# 数据库驱动 +phoenix.driverClassName=org.apache.phoenix.jdbc.PhoenixDriver +# zookeeper地址 +phoenix.url=jdbc:phoenix:192.168.0.105:2181 +``` + +### 2.4 配置数据源和会话工厂 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 2.5 Mybtais参数配置 + +新建 mybtais 配置文件,按照需求配置额外参数, 更多 settings 配置项可以参考[官方文档](http://www.mybatis.org/mybatis-3/zh/configuration.html) + +```xml + + + + + + + + + + + + +``` + +### 2.6 查询接口 + +```java +public interface PopulationDao { + + List queryAll(); + + void save(USPopulation USPopulation); + + USPopulation queryByStateAndCity(@Param("state") String state, @Param("city") String city); + + void deleteByStateAndCity(@Param("state") String state, @Param("city") String city); +} +``` + +```xml + + + + + + + + + UPSERT INTO us_population VALUES( #{state}, #{city}, #{population} ) + + + + + + DELETE FROM us_population WHERE state=#{state} AND city = #{city} + + + +``` + +### 2.7 单元测试 + +```java +@RunWith(SpringRunner.class) +@ContextConfiguration({"classpath:springApplication.xml"}) +public class PopulationDaoTest { + + @Autowired + private PopulationDao populationDao; + + @Test + public void queryAll() { + List USPopulationList = populationDao.queryAll(); + if (USPopulationList != null) { + for (USPopulation USPopulation : USPopulationList) { + System.out.println(USPopulation.getCity() + " " + USPopulation.getPopulation()); + } + } + } + + @Test + public void save() { + populationDao.save(new USPopulation("TX", "Dallas", 66666)); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } + + @Test + public void update() { + populationDao.save(new USPopulation("TX", "Dallas", 99999)); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } + + + @Test + public void delete() { + populationDao.deleteByStateAndCity("TX", "Dallas"); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } +} +``` + +## 三、SpringBoot + Mybatis + Phoenix + +### 3.1 项目结构 + +
+ +### 3.2 主要依赖 + +```xml + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 1.3.2 + + + + org.apache.phoenix + phoenix-core + 4.14.0-cdh5.14.2 + + +``` + +spring boot 与 mybatis 版本的对应关系: + +| MyBatis-Spring-Boot-Starter 版本 | MyBatis-Spring 版本 | Spring Boot 版本 | +| -------------------------------- | ------------------- | ---------------- | +| **1.3.x (1.3.1)** | 1.3 or higher | 1.5 or higher | +| **1.2.x (1.2.1)** | 1.3 or higher | 1.4 or higher | +| **1.1.x (1.1.1)** | 1.3 or higher | 1.3 or higher | +| **1.0.x (1.0.2)** | 1.2 or higher | 1.3 or higher | + +### 3.3 配置数据源 + +在 application.yml 中配置数据源,spring boot 2.x 版本默认采用 Hikari 作为数据库连接池,Hikari 是目前 java 平台性能最好的连接池,性能好于 druid。 + +```yaml +spring: + datasource: + #zookeeper 地址 + url: jdbc:phoenix:192.168.0.105:2181 + driver-class-name: org.apache.phoenix.jdbc.PhoenixDriver + + # 如果不想配置对数据库连接池做特殊配置的话,以下关于连接池的配置就不是必须的 + # spring-boot 2.X 默认采用高性能的 Hikari 作为连接池 更多配置可以参考 https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby + type: com.zaxxer.hikari.HikariDataSource + hikari: + # 池中维护的最小空闲连接数 + minimum-idle: 10 + # 池中最大连接数,包括闲置和使用中的连接 + maximum-pool-size: 20 + # 此属性控制从池返回的连接的默认自动提交行为。默认为 true + auto-commit: true + # 允许最长空闲时间 + idle-timeout: 30000 + # 此属性表示连接池的用户定义名称,主要显示在日志记录和 JMX 管理控制台中,以标识池和池配置。 默认值:自动生成 + pool-name: custom-hikari + #此属性控制池中连接的最长生命周期,值 0 表示无限生命周期,默认 1800000 即 30 分钟 + max-lifetime: 1800000 + # 数据库连接超时时间,默认 30 秒,即 30000 + connection-timeout: 30000 + # 连接测试 sql 这个地方需要根据数据库方言差异而配置 例如 oracle 就应该写成 select 1 from dual + connection-test-query: SELECT 1 + +# mybatis 相关配置 +mybatis: + configuration: + # 是否打印 sql 语句 调试的时候可以开启 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +``` + +### 3.4 新建查询接口 + +上面 Spring+Mybatis 我们使用了 XML 的方式来写 SQL,为了体现 Mybatis 支持多种方式,这里使用注解的方式来写 SQL。 + +```java +@Mapper +public interface PopulationDao { + + @Select("SELECT * from us_population") + List queryAll(); + + @Insert("UPSERT INTO us_population VALUES( #{state}, #{city}, #{population} )") + void save(USPopulation USPopulation); + + @Select("SELECT * FROM us_population WHERE state=#{state} AND city = #{city}") + USPopulation queryByStateAndCity(String state, String city); + + + @Delete("DELETE FROM us_population WHERE state=#{state} AND city = #{city}") + void deleteByStateAndCity(String state, String city); +} +``` + +### 3.5 单元测试 + +```java +@RunWith(SpringRunner.class) +@SpringBootTest +public class PopulationTest { + + @Autowired + private PopulationDao populationDao; + + @Test + public void queryAll() { + List USPopulationList = populationDao.queryAll(); + if (USPopulationList != null) { + for (USPopulation USPopulation : USPopulationList) { + System.out.println(USPopulation.getCity() + " " + USPopulation.getPopulation()); + } + } + } + + @Test + public void save() { + populationDao.save(new USPopulation("TX", "Dallas", 66666)); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } + + @Test + public void update() { + populationDao.save(new USPopulation("TX", "Dallas", 99999)); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } + + + @Test + public void delete() { + populationDao.deleteByStateAndCity("TX", "Dallas"); + USPopulation usPopulation = populationDao.queryByStateAndCity("TX", "Dallas"); + System.out.println(usPopulation); + } + +} + +``` + + + +## 附:建表语句 + +上面单元测试涉及到的测试表的建表语句如下: + +```sql +CREATE TABLE IF NOT EXISTS us_population ( + state CHAR(2) NOT NULL, + city VARCHAR NOT NULL, + population BIGINT + CONSTRAINT my_pk PRIMARY KEY (state, city)); + +-- 测试数据 +UPSERT INTO us_population VALUES('NY','New York',8143197); +UPSERT INTO us_population VALUES('CA','Los Angeles',3844829); +UPSERT INTO us_population VALUES('IL','Chicago',2842518); +UPSERT INTO us_population VALUES('TX','Houston',2016582); +UPSERT INTO us_population VALUES('PA','Philadelphia',1463281); +UPSERT INTO us_population VALUES('AZ','Phoenix',1461575); +UPSERT INTO us_population VALUES('TX','San Antonio',1256509); +UPSERT INTO us_population VALUES('CA','San Diego',1255540); +UPSERT INTO us_population VALUES('CA','San Jose',912332); +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\345\237\272\346\234\254\344\275\277\347\224\250.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\345\237\272\346\234\254\344\275\277\347\224\250.md" new file mode 100644 index 0000000..0a5a831 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\345\237\272\346\234\254\344\275\277\347\224\250.md" @@ -0,0 +1,387 @@ +# Sqoop基本使用 + + + + +## 一、Sqoop 基本命令 + +### 1. 查看所有命令 + +```shell +# sqoop help +``` + +
+ +
+ +### 2. 查看某条命令的具体使用方法 + +```shell +# sqoop help 命令名 +``` + + + +## 二、Sqoop 与 MySQL + +### 1. 查询MySQL所有数据库 + +通常用于 Sqoop 与 MySQL 连通测试: + +```shell +sqoop list-databases \ +--connect jdbc:mysql://hadoop001:3306/ \ +--username root \ +--password root +``` + +
+ +
+ +### 2. 查询指定数据库中所有数据表 + +```shell +sqoop list-tables \ +--connect jdbc:mysql://hadoop001:3306/mysql \ +--username root \ +--password root +``` + + + +## 三、Sqoop 与 HDFS + +### 3.1 MySQL数据导入到HDFS + +#### 1. 导入命令 + +示例:导出 MySQL 数据库中的 `help_keyword` 表到 HDFS 的 `/sqoop` 目录下,如果导入目录存在则先删除再导入,使用 3 个 `map tasks` 并行导入。 + +> 注:help_keyword 是 MySQL 内置的一张字典表,之后的示例均使用这张表。 + +```shell +sqoop import \ +--connect jdbc:mysql://hadoop001:3306/mysql \ +--username root \ +--password root \ +--table help_keyword \ # 待导入的表 +--delete-target-dir \ # 目标目录存在则先删除 +--target-dir /sqoop \ # 导入的目标目录 +--fields-terminated-by '\t' \ # 指定导出数据的分隔符 +-m 3 # 指定并行执行的 map tasks 数量 +``` + +日志输出如下,可以看到输入数据被平均 `split` 为三份,分别由三个 `map task` 进行处理。数据默认以表的主键列作为拆分依据,如果你的表没有主键,有以下两种方案: + ++ 添加 `-- autoreset-to-one-mapper` 参数,代表只启动一个 `map task`,即不并行执行; ++ 若仍希望并行执行,则可以使用 `--split-by ` 指明拆分数据的参考列。 + +
+ +#### 2. 导入验证 + +```shell +# 查看导入后的目录 +hadoop fs -ls -R /sqoop +# 查看导入内容 +hadoop fs -text /sqoop/part-m-00000 +``` + +查看 HDFS 导入目录,可以看到表中数据被分为 3 部分进行存储,这是由指定的并行度决定的。 + +
+ +
+ +### 3.2 HDFS数据导出到MySQL + +```shell +sqoop export \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --table help_keyword_from_hdfs \ # 导出数据存储在 MySQL 的 help_keyword_from_hdf 的表中 + --export-dir /sqoop \ + --input-fields-terminated-by '\t'\ + --m 3 +``` + +表必须预先创建,建表语句如下: + +```sql +CREATE TABLE help_keyword_from_hdfs LIKE help_keyword ; +``` + + + +## 四、Sqoop 与 Hive + +### 4.1 MySQL数据导入到Hive + +Sqoop 导入数据到 Hive 是通过先将数据导入到 HDFS 上的临时目录,然后再将数据从 HDFS 上 `Load` 到 Hive 中,最后将临时目录删除。可以使用 `target-dir` 来指定临时目录。 + +#### 1. 导入命令 + +```shell +sqoop import \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --table help_keyword \ # 待导入的表 + --delete-target-dir \ # 如果临时目录存在删除 + --target-dir /sqoop_hive \ # 临时目录位置 + --hive-database sqoop_test \ # 导入到 Hive 的 sqoop_test 数据库,数据库需要预先创建。不指定则默认为 default 库 + --hive-import \ # 导入到 Hive + --hive-overwrite \ # 如果 Hive 表中有数据则覆盖,这会清除表中原有的数据,然后再写入 + -m 3 # 并行度 +``` + +导入到 Hive 中的 `sqoop_test` 数据库需要预先创建,不指定则默认使用 Hive 中的 `default` 库。 + +```shell + # 查看 hive 中的所有数据库 + hive> SHOW DATABASES; + # 创建 sqoop_test 数据库 + hive> CREATE DATABASE sqoop_test; +``` + +#### 2. 导入验证 + +```shell +# 查看 sqoop_test 数据库的所有表 + hive> SHOW TABLES IN sqoop_test; +# 查看表中数据 + hive> SELECT * FROM sqoop_test.help_keyword; +``` + +
+ +#### 3. 可能出现的问题 + +
+ +
+ +如果执行报错 `java.io.IOException: java.lang.ClassNotFoundException: org.apache.hadoop.hive.conf.HiveConf`,则需将 Hive 安装目录下 `lib` 下的 `hive-exec-**.jar` 放到 sqoop 的 `lib` 。 + +```shell +[root@hadoop001 lib]# ll hive-exec-* +-rw-r--r--. 1 1106 4001 19632031 11 月 13 21:45 hive-exec-1.1.0-cdh5.15.2.jar +[root@hadoop001 lib]# cp hive-exec-1.1.0-cdh5.15.2.jar ${SQOOP_HOME}/lib +``` + +
+ +### 4.2 Hive 导出数据到MySQL + +由于 Hive 的数据是存储在 HDFS 上的,所以 Hive 导入数据到 MySQL,实际上就是 HDFS 导入数据到 MySQL。 + +#### 1. 查看Hive表在HDFS的存储位置 + +```shell +# 进入对应的数据库 +hive> use sqoop_test; +# 查看表信息 +hive> desc formatted help_keyword; +``` + +`Location` 属性为其存储位置: + +
+ +这里可以查看一下这个目录,文件结构如下: + +
+ +#### 3.2 执行导出命令 + +```shell +sqoop export \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --table help_keyword_from_hive \ + --export-dir /user/hive/warehouse/sqoop_test.db/help_keyword \ + -input-fields-terminated-by '\001' \ # 需要注意的是 hive 中默认的分隔符为 \001 + --m 3 +``` +MySQL 中的表需要预先创建: + +```sql +CREATE TABLE help_keyword_from_hive LIKE help_keyword ; +``` + + + +## 五、Sqoop 与 HBase + +> 本小节只讲解从 RDBMS 导入数据到 HBase,因为暂时没有命令能够从 HBase 直接导出数据到 RDBMS。 + +### 5.1 MySQL导入数据到HBase + +#### 1. 导入数据 + +将 `help_keyword` 表中数据导入到 HBase 上的 `help_keyword_hbase` 表中,使用原表的主键 `help_keyword_id` 作为 `RowKey`,原表的所有列都会在 `keywordInfo` 列族下,目前只支持全部导入到一个列族下,不支持分别指定列族。 + +```shell +sqoop import \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --table help_keyword \ # 待导入的表 + --hbase-table help_keyword_hbase \ # hbase 表名称,表需要预先创建 + --column-family keywordInfo \ # 所有列导入到 keywordInfo 列族下 + --hbase-row-key help_keyword_id # 使用原表的 help_keyword_id 作为 RowKey +``` + +导入的 HBase 表需要预先创建: + +```shell +# 查看所有表 +hbase> list +# 创建表 +hbase> create 'help_keyword_hbase', 'keywordInfo' +# 查看表信息 +hbase> desc 'help_keyword_hbase' +``` + +#### 2. 导入验证 + +使用 `scan` 查看表数据: + +
+ + + + + +## 六、全库导出 + +Sqoop 支持通过 `import-all-tables` 命令进行全库导出到 HDFS/Hive,但需要注意有以下两个限制: + ++ 所有表必须有主键;或者使用 `--autoreset-to-one-mapper`,代表只启动一个 `map task`; ++ 你不能使用非默认的分割列,也不能通过 WHERE 子句添加任何限制。 + +> 第二点解释得比较拗口,这里列出官方原本的说明: +> +> + You must not intend to use non-default splitting column, nor impose any conditions via a `WHERE` clause. + +全库导出到 HDFS: + +```shell +sqoop import-all-tables \ + --connect jdbc:mysql://hadoop001:3306/数据库名 \ + --username root \ + --password root \ + --warehouse-dir /sqoop_all \ # 每个表会单独导出到一个目录,需要用此参数指明所有目录的父目录 + --fields-terminated-by '\t' \ + -m 3 +``` + +全库导出到 Hive: + +```shell +sqoop import-all-tables -Dorg.apache.sqoop.splitter.allow_text_splitter=true \ + --connect jdbc:mysql://hadoop001:3306/数据库名 \ + --username root \ + --password root \ + --hive-database sqoop_test \ # 导出到 Hive 对应的库 + --hive-import \ + --hive-overwrite \ + -m 3 +``` + + + +## 七、Sqoop 数据过滤 + +### 7.1 query参数 + +Sqoop 支持使用 `query` 参数定义查询 SQL,从而可以导出任何想要的结果集。使用示例如下: + +```shell +sqoop import \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --query 'select * from help_keyword where $CONDITIONS and help_keyword_id < 50' \ + --delete-target-dir \ + --target-dir /sqoop_hive \ + --hive-database sqoop_test \ # 指定导入目标数据库 不指定则默认使用 Hive 中的 default 库 + --hive-table filter_help_keyword \ # 指定导入目标表 + --split-by help_keyword_id \ # 指定用于 split 的列 + --hive-import \ # 导入到 Hive + --hive-overwrite \ 、 + -m 3 +``` + +在使用 `query` 进行数据过滤时,需要注意以下三点: + ++ 必须用 `--hive-table` 指明目标表; ++ 如果并行度 `-m` 不为 1 或者没有指定 `--autoreset-to-one-mapper`,则需要用 ` --split-by ` 指明参考列; ++ SQL 的 `where` 字句必须包含 `$CONDITIONS`,这是固定写法,作用是动态替换。 + + + +### 7.2 增量导入 + +```shell +sqoop import \ + --connect jdbc:mysql://hadoop001:3306/mysql \ + --username root \ + --password root \ + --table help_keyword \ + --target-dir /sqoop_hive \ + --hive-database sqoop_test \ + --incremental append \ # 指明模式 + --check-column help_keyword_id \ # 指明用于增量导入的参考列 + --last-value 300 \ # 指定参考列上次导入的最大值 + --hive-import \ + -m 3 +``` + +`incremental` 参数有以下两个可选的选项: + ++ **append**:要求参考列的值必须是递增的,所有大于 `last-value` 的值都会被导入; ++ **lastmodified**:要求参考列的值必须是 `timestamp` 类型,且插入数据时候要在参考列插入当前时间戳,更新数据时也要更新参考列的时间戳,所有时间晚于 ``last-value`` 的数据都会被导入。 + +通过上面的解释我们可以看出来,其实 Sqoop 的增量导入并没有太多神器的地方,就是依靠维护的参考列来判断哪些是增量数据。当然我们也可以使用上面介绍的 `query` 参数来进行手动的增量导出,这样反而更加灵活。 + + + +## 八、类型支持 + +Sqoop 默认支持数据库的大多数字段类型,但是某些特殊类型是不支持的。遇到不支持的类型,程序会抛出异常 `Hive does not support the SQL type for column xxx` 异常,此时可以通过下面两个参数进行强制类型转换: + ++ **--map-column-java\** :重写 SQL 到 Java 类型的映射; ++ **--map-column-hive \** : 重写 Hive 到 Java 类型的映射。 + +示例如下,将原先 `id` 字段强制转为 String 类型,`value` 字段强制转为 Integer 类型: + +``` +$ sqoop import ... --map-column-java id=String,value=Integer +``` + + + + + +## 参考资料 + +[Sqoop User Guide (v1.4.7)](http://sqoop.apache.org/docs/1.4.7/SqoopUserGuide.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\347\256\200\344\273\213\344\270\216\345\256\211\350\243\205.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\347\256\200\344\273\213\344\270\216\345\256\211\350\243\205.md" new file mode 100644 index 0000000..638dd3e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Sqoop\347\256\200\344\273\213\344\270\216\345\256\211\350\243\205.md" @@ -0,0 +1,147 @@ +# Sqoop 简介与安装 + + + + +## 一、Sqoop 简介 + +Sqoop 是一个常用的数据迁移工具,主要用于在不同存储系统之间实现数据的导入与导出: + ++ 导入数据:从 MySQL,Oracle 等关系型数据库中导入数据到 HDFS、Hive、HBase 等分布式文件存储系统中; + ++ 导出数据:从 分布式文件系统中导出数据到关系数据库中。 + +其原理是将执行命令转化成 MapReduce 作业来实现数据的迁移,如下图: + +
+ +## 二、安装 + +版本选择:目前 Sqoop 有 Sqoop 1 和 Sqoop 2 两个版本,但是截至到目前,官方并不推荐使用 Sqoop 2,因为其与 Sqoop 1 并不兼容,且功能还没有完善,所以这里优先推荐使用 Sqoop 1。 + +
+ + + +### 2.1 下载并解压 + +下载所需版本的 Sqoop ,这里我下载的是 `CDH` 版本的 Sqoop 。下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# 下载后进行解压 +tar -zxvf sqoop-1.4.6-cdh5.15.2.tar.gz +``` + +### 2.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export SQOOP_HOME=/usr/app/sqoop-1.4.6-cdh5.15.2 +export PATH=$SQOOP_HOME/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 2.3 修改配置 + +进入安装目录下的 `conf/` 目录,拷贝 Sqoop 的环境配置模板 `sqoop-env.sh.template` + +```shell +# cp sqoop-env-template.sh sqoop-env.sh +``` + +修改 `sqoop-env.sh`,内容如下 (以下配置中 `HADOOP_COMMON_HOME` 和 `HADOOP_MAPRED_HOME` 是必选的,其他的是可选的): + +```shell +# Set Hadoop-specific environment variables here. +#Set path to where bin/hadoop is available +export HADOOP_COMMON_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 + +#Set path to where hadoop-*-core.jar is available +export HADOOP_MAPRED_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 + +#set the path to where bin/hbase is available +export HBASE_HOME=/usr/app/hbase-1.2.0-cdh5.15.2 + +#Set the path to where bin/hive is available +export HIVE_HOME=/usr/app/hive-1.1.0-cdh5.15.2 + +#Set the path for where zookeper config dir is +export ZOOCFGDIR=/usr/app/zookeeper-3.4.13/conf + +``` + +### 2.4 拷贝数据库驱动 + +将 MySQL 驱动包拷贝到 Sqoop 安装目录的 `lib` 目录下, 驱动包的下载地址为 https://dev.mysql.com/downloads/connector/j/ 。在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下我也上传了一份,有需要的话可以自行下载。 + +
+ + + +### 2.5 验证 + +由于已经将 sqoop 的 `bin` 目录配置到环境变量,直接使用以下命令验证是否配置成功: + +```shell +# sqoop version +``` + +出现对应的版本信息则代表配置成功: + +
+ +这里出现的两个 `Warning` 警告是因为我们本身就没有用到 `HCatalog` 和 `Accumulo`,忽略即可。Sqoop 在启动时会去检查环境变量中是否有配置这些软件,如果想去除这些警告,可以修改 `bin/configure-sqoop`,注释掉不必要的检查。 + +```shell +# Check: If we can't find our dependencies, give up here. +if [ ! -d "${HADOOP_COMMON_HOME}" ]; then + echo "Error: $HADOOP_COMMON_HOME does not exist!" + echo 'Please set $HADOOP_COMMON_HOME to the root of your Hadoop installation.' + exit 1 +fi +if [ ! -d "${HADOOP_MAPRED_HOME}" ]; then + echo "Error: $HADOOP_MAPRED_HOME does not exist!" + echo 'Please set $HADOOP_MAPRED_HOME to the root of your Hadoop MapReduce installation.' + exit 1 +fi + +## Moved to be a runtime check in sqoop. +if [ ! -d "${HBASE_HOME}" ]; then + echo "Warning: $HBASE_HOME does not exist! HBase imports will fail." + echo 'Please set $HBASE_HOME to the root of your HBase installation.' +fi + +## Moved to be a runtime check in sqoop. +if [ ! -d "${HCAT_HOME}" ]; then + echo "Warning: $HCAT_HOME does not exist! HCatalog jobs will fail." + echo 'Please set $HCAT_HOME to the root of your HCatalog installation.' +fi + +if [ ! -d "${ACCUMULO_HOME}" ]; then + echo "Warning: $ACCUMULO_HOME does not exist! Accumulo imports will fail." + echo 'Please set $ACCUMULO_HOME to the root of your Accumulo installation.' +fi +if [ ! -d "${ZOOKEEPER_HOME}" ]; then + echo "Warning: $ZOOKEEPER_HOME does not exist! Accumulo imports will fail." + echo 'Please set $ZOOKEEPER_HOME to the root of your Zookeeper installation.' +fi +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\344\270\211\347\247\215\346\211\223\345\214\205\346\226\271\345\274\217\345\257\271\346\257\224\345\210\206\346\236\220.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\344\270\211\347\247\215\346\211\223\345\214\205\346\226\271\345\274\217\345\257\271\346\257\224\345\210\206\346\236\220.md" new file mode 100644 index 0000000..8e29982 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\344\270\211\347\247\215\346\211\223\345\214\205\346\226\271\345\274\217\345\257\271\346\257\224\345\210\206\346\236\220.md" @@ -0,0 +1,315 @@ +# Storm三种打包方式对比分析 + + + + +## 一、简介 + +在将 Storm Topology 提交到服务器集群运行时,需要先将项目进行打包。本文主要对比分析各种打包方式,并将打包过程中需要注意的事项进行说明。主要打包方式有以下三种: + ++ 第一种:不加任何插件,直接使用 mvn package 打包; ++ 第二种:使用 maven-assembly-plugin 插件进行打包; ++ 第三种:使用 maven-shade-plugin 进行打包。 + +以下分别进行详细的说明。 + + + +## 二、mvn package + +### 2.1 mvn package的局限 + +不在 POM 中配置任何插件,直接使用 `mvn package` 进行项目打包,这对于没有使用外部依赖包的项目是可行的。 + +但如果项目中使用了第三方 JAR 包,就会出现问题,因为 `mvn package` 打包后的 JAR 中是不含有依赖包的,如果此时你提交到服务器上运行,就会出现找不到第三方依赖的异常。 + +如果你想采用这种方式进行打包,但是又使用了第三方 JAR,有没有解决办法?答案是有的,这一点在官方文档的[Command Line Client](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Command-line-client.html) 章节有所讲解,主要解决办法如下。 + +### 2.2 解决办法 + +在使用 `storm jar` 提交 Topology 时,可以使用如下方式指定第三方依赖: + ++ 如果第三方 JAR 包在本地,可以使用 `--jars` 指定; ++ 如果第三方 JAR 包在远程中央仓库,可以使用 `--artifacts` 指定,此时如果想要排除某些依赖,可以使用 `^` 符号。指定后 Storm 会自动到中央仓库进行下载,然后缓存到本地; ++ 如果第三方 JAR 包在其他仓库,还需要使用 `--artifactRepositories` 指明仓库地址,库名和地址使用 `^` 符号分隔。 + +以下是一个包含上面三种情况的命令示例: + +```shell +./bin/storm jar example/storm-starter/storm-starter-topologies-*.jar \ +org.apache.storm.starter.RollingTopWords blobstore-remote2 remote \ +--jars "./external/storm-redis/storm-redis-1.1.0.jar,./external/storm-kafka/storm-kafka-1.1.0.jar" \ +--artifacts "redis.clients:jedis:2.9.0,org.apache.kafka:kafka_2.10:0.8.2.2^org.slf4j:slf4j-log4j12" \ +--artifactRepositories "jboss-repository^http://repository.jboss.com/maven2, \ +HDPRepo^http://repo.hortonworks.com/content/groups/public/" +``` + +这种方式是建立在你能够连接到外网的情况下,如果你的服务器不能连接外网,或者你希望能把项目直接打包成一个 `ALL IN ONE` 的 JAR,即包含所有相关依赖,此时可以采用下面介绍的两个插件。 + +## 三、maven-assembly-plugin插件 + +maven-assembly-plugin 是官方文档中介绍的打包方法,来源于官方文档:[Running Topologies on a Production Cluster](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Running-topologies-on-a-production-cluster.html) + +> If you're using Maven, the [Maven Assembly Plugin](http://maven.apache.org/plugins/maven-assembly-plugin/) can do the packaging for you. Just add this to your pom.xml: +> +> ```xml +> +> maven-assembly-plugin +> +> +> jar-with-dependencies +> +> +> +> com.path.to.main.Class +> +> +> +> +> ``` +> +> Then run mvn assembly:assembly to get an appropriately packaged jar. Make sure you [exclude](http://maven.apache.org/plugins/maven-assembly-plugin/examples/single/including-and-excluding-artifacts.html) the Storm jars since the cluster already has Storm on the classpath. + +官方文档主要说明了以下几点: + +- 使用 maven-assembly-plugin 可以把所有的依赖一并打入到最后的 JAR 中; +- 需要排除掉 Storm 集群环境中已经提供的 Storm jars; +- 通过 ` ` 标签指定主入口类; +- 通过 `` 标签指定打包相关配置。 + +`jar-with-dependencies` 是 Maven[预定义](http://maven.apache.org/plugins/maven-assembly-plugin/descriptor-refs.html#jar-with-dependencies) 的一种最基本的打包配置,其 XML 文件如下: + +```xml + + jar-with-dependencies + + jar + + false + + + / + true + true + runtime + + + +``` + +我们可以通过对该配置文件进行拓展,从而实现更多的功能,比如排除指定的 JAR 等。使用示例如下: + +### 1. 引入插件 + +在 POM.xml 中引入插件,并指定打包格式的配置文件为 `assembly.xml`(名称可自定义): + +```xml + + + + maven-assembly-plugin + + + src/main/resources/assembly.xml + + + + com.heibaiying.wordcount.ClusterWordCountApp + + + + + + +``` + +`assembly.xml` 拓展自 `jar-with-dependencies.xml`,使用了 `` 标签排除 Storm jars,具体内容如下: + +```xml + + + jar-with-dependencies + + + + jar + + + false + + + / + true + true + runtime + + + org.apache.storm:storm-core + + + + +``` + +>在配置文件中不仅可以排除依赖,还可以排除指定的文件,更多的配置规则可以参考官方文档:[Descriptor Format](http://maven.apache.org/plugins/maven-assembly-plugin/assembly.html#) + +### 2. 打包命令 + +采用 maven-assembly-plugin 进行打包时命令如下: + +```shell +# mvn assembly:assembly +``` + +打包后会同时生成两个 JAR 包,其中后缀为 `jar-with-dependencies` 是含有第三方依赖的 JAR 包,后缀是由 `assembly.xml` 中 `` 标签指定的,可以自定义修改。提交该 JAR 到集群环境即可直接使用。 + +
+ + + +## 四、maven-shade-plugin插件 + +### 4.1 官方文档说明 + +第三种方式是使用 maven-shade-plugin,既然已经有了 maven-assembly-plugin,为什么还需要 maven-shade-plugin,这一点在官方文档中也是有所说明的,来自于官方对 HDFS 整合讲解的章节[Storm HDFS Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html),原文如下: + +>When packaging your topology, it's important that you use the [maven-shade-plugin](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html) as opposed to the [maven-assembly-plugin](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html). +> +>The shade plugin provides facilities for merging JAR manifest entries, which the hadoop client leverages for URL scheme resolution. +> +>If you experience errors such as the following: +> +>``` +>java.lang.RuntimeException: Error preparing HdfsBolt: No FileSystem for scheme: hdfs +>``` +> +>it's an indication that your topology jar file isn't packaged properly. +> +>If you are using maven to create your topology jar, you should use the following `maven-shade-plugin` configuration to create your topology jar。 + +这里第一句就说的比较清晰,在集成 HDFS 时候,你必须使用 maven-shade-plugin 来代替 maven-assembly-plugin,否则会抛出 RuntimeException 异常。 + +采用 maven-shade-plugin 打包有很多好处,比如你的工程依赖很多的 JAR 包,而被依赖的 JAR 又会依赖其他的 JAR 包,这样,当工程中依赖到不同的版本的 JAR 时,并且 JAR 中具有相同名称的资源文件时,shade 插件会尝试将所有资源文件打包在一起时,而不是和 assembly 一样执行覆盖操作。 + +### 4.2 配置 + +采用 `maven-shade-plugin` 进行打包时候,配置示例如下: + +```xml + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.sf + META-INF/*.DSA + META-INF/*.dsa + META-INF/*.RSA + META-INF/*.rsa + META-INF/*.EC + META-INF/*.ec + META-INF/MSFTSIG.SF + META-INF/MSFTSIG.RSA + + + + + + org.apache.storm:storm-core + + + + + + package + + shade + + + + + + + + + + + +``` + +以上配置示例来源于 Storm Github,这里做一下说明: + +在上面的配置中,排除了部分文件,这是因为有些 JAR 包生成时,会使用 jarsigner 生成文件签名(完成性校验),分为两个文件存放在 META-INF 目录下: + ++ a signature file, with a .SF extension; ++ a signature block file, with a .DSA, .RSA, or .EC extension; + +如果某些包的存在重复引用,这可能会导致在打包时候出现 `Invalid signature file digest for Manifest main attributes` 异常,所以在配置中排除这些文件。 + +### 4.3 打包命令 + +使用 maven-shade-plugin 进行打包的时候,打包命令和普通的一样: + +```shell +# mvn package +``` + +打包后会生成两个 JAR 包,提交到服务器集群时使用 ` 非 original` 开头的 JAR。 + +
+ +## 五、结论 + +通过以上三种打包方式的详细介绍,这里给出最后的结论:**建议使用 maven-shade-plugin 插件进行打包**,因为其通用性最强,操作最简单,并且 Storm Github 中所有[examples](https://github.com/apache/storm/tree/master/examples) 都是采用该方式进行打包。 + + + +## 六、打包注意事项 + +无论采用任何打包方式,都必须排除集群环境中已经提供的 storm jars。这里比较典型的是 storm-core,其在安装目录的 lib 目录下已经存在。 + +
+ + + +如果你不排除 storm-core,通常会抛出下面的异常: + +```properties +Caused by: java.lang.RuntimeException: java.io.IOException: Found multiple defaults.yaml resources. +You're probably bundling the Storm jars with your topology jar. +[jar:file:/usr/app/apache-storm-1.2.2/lib/storm-core-1.2.2.jar!/defaults.yaml, +jar:file:/usr/appjar/storm-hdfs-integration-1.0.jar!/defaults.yaml] + at org.apache.storm.utils.Utils.findAndReadConfigFile(Utils.java:384) + at org.apache.storm.utils.Utils.readDefaultConfig(Utils.java:428) + at org.apache.storm.utils.Utils.readStormConfig(Utils.java:464) + at org.apache.storm.utils.Utils.(Utils.java:178) + ... 39 more +``` + +
+ + + +## 参考资料 + +关于 maven-shade-plugin 的更多配置可以参考: [maven-shade-plugin 入门指南](https://www.jianshu.com/p/7a0e20b30401) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\345\222\214\346\265\201\345\244\204\347\220\206\347\256\200\344\273\213.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\345\222\214\346\265\201\345\244\204\347\220\206\347\256\200\344\273\213.md" new file mode 100644 index 0000000..caec97d --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\345\222\214\346\265\201\345\244\204\347\220\206\347\256\200\344\273\213.md" @@ -0,0 +1,98 @@ +# Storm和流处理简介 + + + + +## 一、Storm + +#### 1.1 简介 + +Storm 是一个开源的分布式实时计算框架,可以以简单、可靠的方式进行大数据流的处理。通常用于实时分析,在线机器学习、持续计算、分布式 RPC、ETL 等场景。Storm 具有以下特点: + ++ 支持水平横向扩展; ++ 具有高容错性,通过 ACK 机制每个消息都不丢失; ++ 处理速度非常快,每个节点每秒能处理超过一百万个 tuples ; ++ 易于设置和操作,并可以与任何编程语言一起使用; ++ 支持本地模式运行,对于开发人员来说非常友好; ++ 支持图形化管理界面。 + + + +#### 1.2 Storm 与 Hadoop对比 + +Hadoop 采用 MapReduce 处理数据,而 MapReduce 主要是对数据进行批处理,这使得 Hadoop 更适合于海量数据离线处理的场景。而 Strom 的设计目标是对数据进行实时计算,这使得其更适合实时数据分析的场景。 + + + +#### 1.3 Storm 与 Spark Streaming对比 + +Spark Streaming 并不是真正意义上的流处理框架。 Spark Streaming 接收实时输入的数据流,并将数据拆分为一系列批次,然后进行微批处理。只不过 Spark Streaming 能够将数据流进行极小粒度的拆分,使得其能够得到接近于流处理的效果,但其本质上还是批处理(或微批处理)。 + +
+ +#### 1.4 Strom 与 Flink对比 + +storm 和 Flink 都是真正意义上的实时计算框架。其对比如下: + +| | storm | flink | +| -------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 状态管理 | 无状态 | 有状态 | +| 窗口支持 | 对事件窗口支持较弱,缓存整个窗口的所有数据,窗口结束时一起计算 | 窗口支持较为完善,自带一些窗口聚合方法,
并且会自动管理窗口状态 | +| 消息投递 | At Most Once
At Least Once | At Most Once
At Least Once
**Exactly Once** | +| 容错方式 | ACK 机制:对每个消息进行全链路跟踪,失败或者超时时候进行重发 | 检查点机制:通过分布式一致性快照机制,
对数据流和算子状态进行保存。在发生错误时,使系统能够进行回滚。 | + + +> 注 : 对于消息投递,一般有以下三种方案: +> + At Most Once : 保证每个消息会被投递 0 次或者 1 次,在这种机制下消息很有可能会丢失; +> + At Least Once : 保证了每个消息会被默认投递多次,至少保证有一次被成功接收,信息可能有重复,但是不会丢失; +> + Exactly Once : 每个消息对于接收者而言正好被接收一次,保证即不会丢失也不会重复。 + + + +## 二、流处理 + +#### 2.1 静态数据处理 + +在流处理之前,数据通常存储在数据库或文件系统中,应用程序根据需要查询或计算数据,这就是传统的静态数据处理架构。Hadoop 采用 HDFS 进行数据存储,采用 MapReduce 进行数据查询或分析,这就是典型的静态数据处理架构。 + +
+ + + +#### 2.2 流处理 + +而流处理则是直接对运动中数据的处理,在接收数据的同时直接计算数据。实际上,在真实世界中的大多数数据都是连续的流,如传感器数据,网站用户活动数据,金融交易数据等等 ,所有这些数据都是随着时间的推移而源源不断地产生。 + +接收和发送数据流并执行应用程序或分析逻辑的系统称为**流处理器**。流处理器的基本职责是确保数据有效流动,同时具备可扩展性和容错能力,Storm 和 Flink 就是其代表性的实现。 + +
+ + + +流处理带来了很多优点: + +- **可以立即对数据做出反应**:降低了数据的滞后性,使得数据更具有时效性,更能反映对未来的预期; + +- **可以处理更大的数据量**:直接处理数据流,并且只保留数据中有意义的子集,然后将其传送到下一个处理单元,通过逐级过滤数据,从而降低实际需要处理的数据量; + +- **更贴近现实的数据模型**:在实际的环境中,一切数据都是持续变化的,想要通过历史数据推断未来的趋势,必须保证数据的不断输入和模型的持续修正,典型的就是金融市场、股票市场,流处理能更好地处理这些场景下对数据连续性和及时性的需求; + +- **分散和分离基础设施**:流式处理减少了对大型数据库的需求。每个流处理程序通过流处理框架维护了自己的数据和状态,这使其更适合于当下最流行的微服务架构。 + + + + + +## 参考资料 + +1. [What is stream processing?](https://www.ververica.com/what-is-stream-processing) +2. [流计算框架 Flink 与 Storm 的性能对比](http://bigdata.51cto.com/art/201711/558416.htm) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\346\240\270\345\277\203\346\246\202\345\277\265\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\346\240\270\345\277\203\346\246\202\345\277\265\350\257\246\350\247\243.md" new file mode 100644 index 0000000..18795ea --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\346\240\270\345\277\203\346\246\202\345\277\265\350\257\246\350\247\243.md" @@ -0,0 +1,159 @@ +# Storm 核心概念详解 + + + +## 一、Storm核心概念 + +
+ +### 1.1 Topologies(拓扑) + +一个完整的 Storm 流处理程序被称为 Storm topology(拓扑)。它是一个是由 `Spouts` 和 `Bolts` 通过 `Stream` 连接起来的有向无环图,Storm 会保持每个提交到集群的 topology 持续地运行,从而处理源源不断的数据流,直到你将主动其杀死 (kill) 为止。 + +### 1.2 Streams(流) + +`Stream` 是 Storm 中的核心概念。一个 `Stream` 是一个无界的、以分布式方式并行创建和处理的 `Tuple` 序列。Tuple 可以包含大多数基本类型以及自定义类型的数据。简单来说,Tuple 就是流数据的实际载体,而 Stream 就是一系列 Tuple。 + +### 1.3 Spouts + +`Spouts` 是流数据的源头,一个 Spout 可以向不止一个 `Streams` 中发送数据。`Spout` 通常分为**可靠**和**不可靠**两种:可靠的 ` Spout` 能够在失败时重新发送 Tuple, 不可靠的 `Spout` 一旦把 Tuple 发送出去就置之不理了。 + +### 1.4 Bolts + +`Bolts` 是流数据的处理单元,它可以从一个或者多个 `Streams` 中接收数据,处理完成后再发射到新的 `Streams` 中。`Bolts` 可以执行过滤 (filtering),聚合 (aggregations),连接 (joins) 等操作,并能与文件系统或数据库进行交互。 + +### 1.5 Stream groupings(分组策略) + +
+ +`spouts` 和 `bolts` 在集群上执行任务时,是由多个 Task 并行执行 (如上图,每一个圆圈代表一个 Task)。当一个 Tuple 需要从 Bolt A 发送给 Bolt B 执行的时候,程序如何知道应该发送给 Bolt B 的哪一个 Task 执行呢? + +这是由 Stream groupings 分组策略来决定的,Storm 中一共有如下 8 个内置的 Stream Grouping。当然你也可以通过实现 `CustomStreamGrouping` 接口来实现自定义 Stream 分组策略。 + +1. **Shuffle grouping** + + Tuples 随机的分发到每个 Bolt 的每个 Task 上,每个 Bolt 获取到等量的 Tuples。 + +2. **Fields grouping** + + Streams 通过 grouping 指定的字段 (field) 来分组。假设通过 `user-id` 字段进行分区,那么具有相同 `user-id` 的 Tuples 就会发送到同一个 Task。 + +3. **Partial Key grouping** + + Streams 通过 grouping 中指定的字段 (field) 来分组,与 `Fields Grouping` 相似。但是对于两个下游的 Bolt 来说是负载均衡的,可以在输入数据不平均的情况下提供更好的优化。 + +4. **All grouping** + + Streams 会被所有的 Bolt 的 Tasks 进行复制。由于存在数据重复处理,所以需要谨慎使用。 + +5. **Global grouping** + + 整个 Streams 会进入 Bolt 的其中一个 Task,通常会进入 id 最小的 Task。 + +6. **None grouping** + + 当前 None grouping 和 Shuffle grouping 等价,都是进行随机分发。 + +7. **Direct grouping** + + Direct grouping 只能被用于 direct streams 。使用这种方式需要由 Tuple 的生产者直接指定由哪个 Task 进行处理。 + +8. **Local or shuffle grouping** + + 如果目标 Bolt 有 Tasks 和当前 Bolt 的 Tasks 处在同一个 Worker 进程中,那么则优先将 Tuple Shuffled 到处于同一个进程的目标 Bolt 的 Tasks 上,这样可以最大限度地减少网络传输。否则,就和普通的 `Shuffle Grouping` 行为一致。 + + + +## 二、Storm架构详解 + +
+ +### 2.1 Nimbus进程 + + 也叫做 Master Node,是 Storm 集群工作的全局指挥官。主要功能如下: + +1. 通过 Thrift 接口,监听并接收 Client 提交的 Topology; +2. 根据集群 Workers 的资源情况,将 Client 提交的 Topology 进行任务分配,分配结果写入 Zookeeper; +3. 通过 Thrift 接口,监听 Supervisor 的下载 Topology 代码的请求,并提供下载 ; +4. 通过 Thrift 接口,监听 UI 对统计信息的读取,从 Zookeeper 上读取统计信息,返回给 UI; +5. 若进程退出后,立即在本机重启,则不影响集群运行。 + + + +### 2.2 Supervisor进程 + +也叫做 Worker Node , 是 Storm 集群的资源管理者,按需启动 Worker 进程。主要功能如下: + +1. 定时从 Zookeeper 检查是否有新 Topology 代码未下载到本地 ,并定时删除旧 Topology 代码 ; +2. 根据 Nimbus 的任务分配计划,在本机按需启动 1 个或多个 Worker 进程,并监控所有的 Worker 进程的情况; +3. 若进程退出,立即在本机重启,则不影响集群运行。 + + + +### 2.3 zookeeper的作用 + +Nimbus 和 Supervisor 进程都被设计为**快速失败**(遇到任何意外情况时进程自毁)和**无状态**(所有状态保存在 Zookeeper 或磁盘上)。 这样设计的好处就是如果它们的进程被意外销毁,那么在重新启动后,就只需要从 Zookeeper 上获取之前的状态数据即可,并不会造成任何数据丢失。 + + + +### 2.4 Worker进程 + +Storm 集群的任务构造者 ,构造 Spoult 或 Bolt 的 Task 实例,启动 Executor 线程。主要功能如下: + +1. 根据 Zookeeper 上分配的 Task,在本进程中启动 1 个或多个 Executor 线程,将构造好的 Task 实例交给 Executor 去运行; +2. 向 Zookeeper 写入心跳 ; +3. 维持传输队列,发送 Tuple 到其他的 Worker ; +4. 若进程退出,立即在本机重启,则不影响集群运行。 + + + +### 2.5 Executor线程 + +Storm 集群的任务执行者 ,循环执行 Task 代码。主要功能如下: + +1. 执行 1 个或多个 Task; +2. 执行 Acker 机制,负责发送 Task 处理状态给对应 Spout 所在的 worker。 + + + +### 2.6 并行度 + +
+ +1 个 Worker 进程执行的是 1 个 Topology 的子集,不会出现 1 个 Worker 为多个 Topology 服务的情况,因此 1 个运行中的 Topology 就是由集群中多台物理机上的多个 Worker 进程组成的。1 个 Worker 进程会启动 1 个或多个 Executor 线程来执行 1 个 Topology 的 Component(组件,即 Spout 或 Bolt)。 + +Executor 是 1 个被 Worker 进程启动的单独线程。每个 Executor 会运行 1 个 Component 中的一个或者多个 Task。 + +Task 是组成 Component 的代码单元。Topology 启动后,1 个 Component 的 Task 数目是固定不变的,但该 Component 使用的 Executor 线程数可以动态调整(例如:1 个 Executor 线程可以执行该 Component 的 1 个或多个 Task 实例)。这意味着,对于 1 个 Component 来说,`#threads<=#tasks`(线程数小于等于 Task 数目)这样的情况是存在的。默认情况下 Task 的数目等于 Executor 线程数,即 1 个 Executor 线程只运行 1 个 Task。 + +**总结如下:** + ++ 一个运行中的 Topology 由集群中的多个 Worker 进程组成的; ++ 在默认情况下,每个 Worker 进程默认启动一个 Executor 线程; ++ 在默认情况下,每个 Executor 默认启动一个 Task 线程; ++ Task 是组成 Component 的代码单元。 + + + +## 参考资料 + +1. [storm documentation -> Concepts](http://storm.apache.org/releases/1.2.2/Concepts.html) + +2. [Internal Working of Apache Storm](https://www.spritle.com/blogs/2016/04/04/apache-storm/) +3. [Understanding the Parallelism of a Storm Topology](http://storm.apache.org/releases/1.2.2/Understanding-the-parallelism-of-a-Storm-topology.html) +4. [Storm nimbus 单节点宕机的处理](https://blog.csdn.net/daiyutage/article/details/52049519) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\347\274\226\347\250\213\346\250\241\345\236\213\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\347\274\226\347\250\213\346\250\241\345\236\213\350\257\246\350\247\243.md" new file mode 100644 index 0000000..8d78856 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\347\274\226\347\250\213\346\250\241\345\236\213\350\257\246\350\247\243.md" @@ -0,0 +1,511 @@ +# Storm 编程模型 + + + + + + + +## 一、简介 + +下图为 Strom 的运行流程图,在开发 Storm 流处理程序时,我们需要采用内置或自定义实现 `spout`(数据源) 和 `bolt`(处理单元),并通过 `TopologyBuilder` 将它们之间进行关联,形成 `Topology`。 + +
+ +## 二、IComponent接口 + +`IComponent` 接口定义了 Topology 中所有组件 (spout/bolt) 的公共方法,自定义的 spout 或 bolt 必须直接或间接实现这个接口。 + +```java +public interface IComponent extends Serializable { + + /** + * 声明此拓扑的所有流的输出模式。 + * @param declarer 这用于声明输出流 id,输出字段以及每个输出流是否是直接流(direct stream) + */ + void declareOutputFields(OutputFieldsDeclarer declarer); + + /** + * 声明此组件的配置。 + * + */ + Map getComponentConfiguration(); + +} +``` + +## 三、Spout + +### 3.1 ISpout接口 + +自定义的 spout 需要实现 `ISpout` 接口,它定义了 spout 的所有可用方法: + +```java +public interface ISpout extends Serializable { + /** + * 组件初始化时候被调用 + * + * @param conf ISpout 的配置 + * @param context 应用上下文,可以通过其获取任务 ID 和组件 ID,输入和输出信息等。 + * @param collector 用来发送 spout 中的 tuples,它是线程安全的,建议保存为此 spout 对象的实例变量 + */ + void open(Map conf, TopologyContext context, SpoutOutputCollector collector); + + /** + * ISpout 将要被关闭的时候调用。但是其不一定会被执行,如果在集群环境中通过 kill -9 杀死进程时其就无法被执行。 + */ + void close(); + + /** + * 当 ISpout 从停用状态激活时被调用 + */ + void activate(); + + /** + * 当 ISpout 停用时候被调用 + */ + void deactivate(); + + /** + * 这是一个核心方法,主要通过在此方法中调用 collector 将 tuples 发送给下一个接收器,这个方法必须是非阻塞的。 + * nextTuple/ack/fail/是在同一个线程中执行的,所以不用考虑线程安全方面。当没有 tuples 发出时应该让 + * nextTuple 休眠 (sleep) 一下,以免浪费 CPU。 + */ + void nextTuple(); + + /** + * 通过 msgId 进行 tuples 处理成功的确认,被确认后的 tuples 不会再次被发送 + */ + void ack(Object msgId); + + /** + * 通过 msgId 进行 tuples 处理失败的确认,被确认后的 tuples 会再次被发送进行处理 + */ + void fail(Object msgId); +} +``` + +### 3.2 BaseRichSpout抽象类 + +**通常情况下,我们实现自定义的 Spout 时不会直接去实现 `ISpout` 接口,而是继承 `BaseRichSpout`。**`BaseRichSpout` 继承自 `BaseCompont`,同时实现了 `IRichSpout` 接口。 + +
+ +`IRichSpout` 接口继承自 `ISpout` 和 `IComponent`,自身并没有定义任何方法: + +```java +public interface IRichSpout extends ISpout, IComponent { + +} +``` + +`BaseComponent` 抽象类空实现了 `IComponent` 中 `getComponentConfiguration` 方法: + +```java +public abstract class BaseComponent implements IComponent { + @Override + public Map getComponentConfiguration() { + return null; + } +} +``` + +`BaseRichSpout` 继承自 `BaseCompont` 类并实现了 `IRichSpout` 接口,并且空实现了其中部分方法: + +```java +public abstract class BaseRichSpout extends BaseComponent implements IRichSpout { + @Override + public void close() {} + + @Override + public void activate() {} + + @Override + public void deactivate() {} + + @Override + public void ack(Object msgId) {} + + @Override + public void fail(Object msgId) {} +} +``` + +通过这样的设计,我们在继承 `BaseRichSpout` 实现自定义 spout 时,就只有三个方法必须实现: + ++ **open** : 来源于 ISpout,可以通过此方法获取用来发送 tuples 的 `SpoutOutputCollector`; ++ **nextTuple** :来源于 ISpout,必须在此方法内部发送 tuples; ++ **declareOutputFields** :来源于 IComponent,声明发送的 tuples 的名称,这样下一个组件才能知道如何接受。 + + + +## 四、Bolt + +bolt 接口的设计与 spout 的类似: + +### 4.1 IBolt 接口 + +```java + /** + * 在客户端计算机上创建的 IBolt 对象。会被被序列化到 topology 中(使用 Java 序列化),并提交给集群的主机(Nimbus)。 + * Nimbus 启动 workers 反序列化对象,调用 prepare,然后开始处理 tuples。 + */ + +public interface IBolt extends Serializable { + /** + * 组件初始化时候被调用 + * + * @param conf storm 中定义的此 bolt 的配置 + * @param context 应用上下文,可以通过其获取任务 ID 和组件 ID,输入和输出信息等。 + * @param collector 用来发送 spout 中的 tuples,它是线程安全的,建议保存为此 spout 对象的实例变量 + */ + void prepare(Map stormConf, TopologyContext context, OutputCollector collector); + + /** + * 处理单个 tuple 输入。 + * + * @param Tuple 对象包含关于它的元数据(如来自哪个组件/流/任务) + */ + void execute(Tuple input); + + /** + * IBolt 将要被关闭的时候调用。但是其不一定会被执行,如果在集群环境中通过 kill -9 杀死进程时其就无法被执行。 + */ + void cleanup(); +``` + + + +### 4.2 BaseRichBolt抽象类 + +同样的,在实现自定义 bolt 时,通常是继承 `BaseRichBolt` 抽象类来实现。`BaseRichBolt` 继承自 `BaseComponent` 抽象类并实现了 `IRichBolt` 接口。 + +
+ +`IRichBolt` 接口继承自 `IBolt` 和 `IComponent`,自身并没有定义任何方法: + +``` +public interface IRichBolt extends IBolt, IComponent { + +} +``` + +通过这样的设计,在继承 `BaseRichBolt` 实现自定义 bolt 时,就只需要实现三个必须的方法: + +- **prepare**: 来源于 IBolt,可以通过此方法获取用来发送 tuples 的 `OutputCollector`; +- **execute**:来源于 IBolt,处理 tuples 和发送处理完成的 tuples; +- **declareOutputFields** :来源于 IComponent,声明发送的 tuples 的名称,这样下一个组件才能知道如何接收。 + + + +## 五、词频统计案例 + +### 5.1 案例简介 + +这里我们使用自定义的 `DataSourceSpout` 产生词频数据,然后使用自定义的 `SplitBolt` 和 `CountBolt` 来进行词频统计。 + +
+ +> 案例源码下载地址:[storm-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-word-count) + +### 5.2 代码实现 + +#### 1. 项目依赖 + +```xml + + org.apache.storm + storm-core + 1.2.2 + +``` + +#### 2. DataSourceSpout + +```java +public class DataSourceSpout extends BaseRichSpout { + + private List list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive"); + + private SpoutOutputCollector spoutOutputCollector; + + @Override + public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) { + this.spoutOutputCollector = spoutOutputCollector; + } + + @Override + public void nextTuple() { + // 模拟产生数据 + String lineData = productData(); + spoutOutputCollector.emit(new Values(lineData)); + Utils.sleep(1000); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) { + outputFieldsDeclarer.declare(new Fields("line")); + } + + + /** + * 模拟数据 + */ + private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); + } + +} +``` + +上面类使用 `productData` 方法来产生模拟数据,产生数据的格式如下: + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +``` + +#### 3. SplitBolt + +```java +public class SplitBolt extends BaseRichBolt { + + private OutputCollector collector; + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector=collector; + } + + @Override + public void execute(Tuple input) { + String line = input.getStringByField("line"); + String[] words = line.split("\t"); + for (String word : words) { + collector.emit(new Values(word)); + } + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("word")); + } +} +``` + +#### 4. CountBolt + +```java +public class CountBolt extends BaseRichBolt { + + private Map counts = new HashMap<>(); + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + + } + + @Override + public void execute(Tuple input) { + String word = input.getStringByField("word"); + Integer count = counts.get(word); + if (count == null) { + count = 0; + } + count++; + counts.put(word, count); + // 输出 + System.out.print("当前实时统计结果:"); + counts.forEach((key, value) -> System.out.print(key + ":" + value + "; ")); + System.out.println(); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + + } +} +``` + +#### 5. LocalWordCountApp + +通过 TopologyBuilder 将上面定义好的组件进行串联形成 Topology,并提交到本地集群(LocalCluster)运行。通常在开发中,可先用本地模式进行测试,测试完成后再提交到服务器集群运行。 + +```java +public class LocalWordCountApp { + + public static void main(String[] args) { + TopologyBuilder builder = new TopologyBuilder(); + + builder.setSpout("DataSourceSpout", new DataSourceSpout()); + + // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理 + builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout"); + + // 指明将 SplitBolt 的数据发送到 CountBolt 中 处理 + builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt"); + + // 创建本地集群用于测试 这种模式不需要本机安装 storm,直接运行该 Main 方法即可 + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalWordCountApp", + new Config(), builder.createTopology()); + } + +} +``` + + + +#### 6. 运行结果 + +启动 `WordCountApp` 的 main 方法即可运行,采用本地模式 Storm 会自动在本地搭建一个集群,所以启动的过程会稍慢一点,启动成功后即可看到输出日志。 + +
+ + +## 六、提交到服务器集群运行 + +### 6.1 代码更改 + +提交到服务器的代码和本地代码略有不同,提交到服务器集群时需要使用 `StormSubmitter` 进行提交。主要代码如下: + +> 为了结构清晰,这里新建 ClusterWordCountApp 类来演示集群模式的提交。实际开发中可以将两种模式的代码写在同一个类中,通过外部传参来决定启动何种模式。 + +```java +public class ClusterWordCountApp { + + public static void main(String[] args) { + TopologyBuilder builder = new TopologyBuilder(); + + builder.setSpout("DataSourceSpout", new DataSourceSpout()); + + // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理 + builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout"); + + // 指明将 SplitBolt 的数据发送到 CountBolt 中 处理 + builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt"); + + // 使用 StormSubmitter 提交 Topology 到服务器集群 + try { + StormSubmitter.submitTopology("ClusterWordCountApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } + +} +``` + +### 6.2 打包上传 + +打包后上传到服务器任意位置,这里我打包后的名称为 `storm-word-count-1.0.jar` + +```shell +# mvn clean package -Dmaven.test.skip=true +``` + +### 6.3 提交Topology + +使用以下命令提交 Topology 到集群: + +```shell +# 命令格式: storm jar jar包位置 主类的全路径 ...可选传参 +storm jar /usr/appjar/storm-word-count-1.0.jar com.heibaiying.wordcount.ClusterWordCountApp +``` + +出现 `successfully` 则代表提交成功: + +
+ +### 6.4 查看Topology与停止Topology(命令行方式) + +```shell +# 查看所有Topology +storm list + +# 停止 storm kill topology-name [-w wait-time-secs] +storm kill ClusterWordCountApp -w 3 +``` + +
+ +### 6.5 查看Topology与停止Topology(界面方式) + +使用 UI 界面同样也可进行停止操作,进入 WEB UI 界面(8080 端口),在 `Topology Summary` 中点击对应 Topology 即可进入详情页面进行操作。 + +
+ + + + + + + + +## 七、关于项目打包的扩展说明 + +### mvn package的局限性 + +在上面的步骤中,我们没有在 POM 中配置任何插件,就直接使用 `mvn package` 进行项目打包,这对于没有使用外部依赖包的项目是可行的。但如果项目中使用了第三方 JAR 包,就会出现问题,因为 `package` 打包后的 JAR 中是不含有依赖包的,如果此时你提交到服务器上运行,就会出现找不到第三方依赖的异常。 + +这时候可能大家会有疑惑,在我们的项目中不是使用了 `storm-core` 这个依赖吗?其实上面之所以我们能运行成功,是因为在 Storm 的集群环境中提供了这个 JAR 包,在安装目录的 lib 目录下: + +
+为了说明这个问题我在 Maven 中引入了一个第三方的 JAR 包,并修改产生数据的方法: + +```xml + + org.apache.commons + commons-lang3 + 3.8.1 + +``` + +`StringUtils.join()` 这个方法在 `commons.lang3` 和 `storm-core` 中都有,原来的代码无需任何更改,只需要在 `import` 时指明使用 `commons.lang3`。 + +```java +import org.apache.commons.lang3.StringUtils; + +private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); +} +``` + +此时直接使用 `mvn clean package` 打包运行,就会抛出下图的异常。因此这种直接打包的方式并不适用于实际的开发,因为实际开发中通常都是需要第三方的 JAR 包。 + +
+ + +想把依赖包一并打入最后的 JAR 中,maven 提供了两个插件来实现,分别是 `maven-assembly-plugin` 和 `maven-shade-plugin`。鉴于本篇文章篇幅已经比较长,且关于 Storm 打包还有很多需要说明的地方,所以关于 Storm 的打包方式单独整理至下一篇文章: + +[Storm 三种打包方式对比分析](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Storm三种打包方式对比分析.md) + +## 参考资料 + +1. [Running Topologies on a Production Cluster](http://storm.apache.org/releases/2.0.0-SNAPSHOT/Running-topologies-on-a-production-cluster.html) +2. [Pre-defined Descriptor Files](http://maven.apache.org/plugins/maven-assembly-plugin/descriptor-refs.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220HBase\345\222\214HDFS.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220HBase\345\222\214HDFS.md" new file mode 100644 index 0000000..d553d7b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220HBase\345\222\214HDFS.md" @@ -0,0 +1,489 @@ +# Storm集成HDFS和HBase + + + +## 一、Storm集成HDFS + +### 1.1 项目结构 + +
+ +> 本用例源码下载地址:[storm-hdfs-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-hdfs-integration) + +### 1.2 项目主要依赖 + +项目主要依赖如下,有两个地方需要注意: + ++ 这里由于我服务器上安装的是 CDH 版本的 Hadoop,在导入依赖时引入的也是 CDH 版本的依赖,需要使用 `` 标签指定 CDH 的仓库地址; ++ `hadoop-common`、`hadoop-client`、`hadoop-hdfs` 均需要排除 `slf4j-log4j12` 依赖,原因是 `storm-core` 中已经有该依赖,不排除的话有 JAR 包冲突的风险; + +```xml + + 1.2.2 + + + + + cloudera + https://repository.cloudera.com/artifactory/cloudera-repos/ + + + + + + org.apache.storm + storm-core + ${storm.version} + + + + org.apache.storm + storm-hdfs + ${storm.version} + + + org.apache.hadoop + hadoop-common + 2.6.0-cdh5.15.2 + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.hadoop + hadoop-client + 2.6.0-cdh5.15.2 + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.hadoop + hadoop-hdfs + 2.6.0-cdh5.15.2 + + + org.slf4j + slf4j-log4j12 + + + + +``` + +### 1.3 DataSourceSpout + +```java +/** + * 产生词频样本的数据源 + */ +public class DataSourceSpout extends BaseRichSpout { + + private List list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive"); + + private SpoutOutputCollector spoutOutputCollector; + + @Override + public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) { + this.spoutOutputCollector = spoutOutputCollector; + } + + @Override + public void nextTuple() { + // 模拟产生数据 + String lineData = productData(); + spoutOutputCollector.emit(new Values(lineData)); + Utils.sleep(1000); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) { + outputFieldsDeclarer.declare(new Fields("line")); + } + + + /** + * 模拟数据 + */ + private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); + } + +} +``` + +产生的模拟数据格式如下: + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +``` + +### 1.4 将数据存储到HDFS + +这里 HDFS 的地址和数据存储路径均使用了硬编码,在实际开发中可以通过外部传参指定,这样程序更为灵活。 + +```java +public class DataToHdfsApp { + + private static final String DATA_SOURCE_SPOUT = "dataSourceSpout"; + private static final String HDFS_BOLT = "hdfsBolt"; + + public static void main(String[] args) { + + // 指定 Hadoop 的用户名 如果不指定,则在 HDFS 创建目录时候有可能抛出无权限的异常 (RemoteException: Permission denied) + System.setProperty("HADOOP_USER_NAME", "root"); + + // 定义输出字段 (Field) 之间的分隔符 + RecordFormat format = new DelimitedRecordFormat() + .withFieldDelimiter("|"); + + // 同步策略: 每 100 个 tuples 之后就会把数据从缓存刷新到 HDFS 中 + SyncPolicy syncPolicy = new CountSyncPolicy(100); + + // 文件策略: 每个文件大小上限 1M,超过限定时,创建新文件并继续写入 + FileRotationPolicy rotationPolicy = new FileSizeRotationPolicy(1.0f, Units.MB); + + // 定义存储路径 + FileNameFormat fileNameFormat = new DefaultFileNameFormat() + .withPath("/storm-hdfs/"); + + // 定义 HdfsBolt + HdfsBolt hdfsBolt = new HdfsBolt() + .withFsUrl("hdfs://hadoop001:8020") + .withFileNameFormat(fileNameFormat) + .withRecordFormat(format) + .withRotationPolicy(rotationPolicy) + .withSyncPolicy(syncPolicy); + + + // 构建 Topology + TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout()); + // save to HDFS + builder.setBolt(HDFS_BOLT, hdfsBolt, 1).shuffleGrouping(DATA_SOURCE_SPOUT); + + + // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动 + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterDataToHdfsApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalDataToHdfsApp", + new Config(), builder.createTopology()); + } + } +} +``` + +### 1.5 启动测试 + +可以用直接使用本地模式运行,也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包,打包命令如下: + +```shell +# mvn clean package -D maven.test.skip=true +``` + +运行后,数据会存储到 HDFS 的 `/storm-hdfs` 目录下。使用以下命令可以查看目录内容: + +```shell +# 查看目录内容 +hadoop fs -ls /storm-hdfs +# 监听文内容变化 +hadoop fs -tail -f /strom-hdfs/文件名 +``` + + + +
+ + + +## 二、Storm集成HBase + +### 2.1 项目结构 + +集成用例: 进行词频统计并将最后的结果存储到 HBase,项目主要结构如下: + +
+ +> 本用例源码下载地址:[storm-hbase-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-hbase-integration) + +### 2.2 项目主要依赖 + +```xml + + 1.2.2 + + + + + + org.apache.storm + storm-core + ${storm.version} + + + + org.apache.storm + storm-hbase + ${storm.version} + + +``` + +### 2.3 DataSourceSpout + +```java +/** + * 产生词频样本的数据源 + */ +public class DataSourceSpout extends BaseRichSpout { + + private List list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive"); + + private SpoutOutputCollector spoutOutputCollector; + + @Override + public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) { + this.spoutOutputCollector = spoutOutputCollector; + } + + @Override + public void nextTuple() { + // 模拟产生数据 + String lineData = productData(); + spoutOutputCollector.emit(new Values(lineData)); + Utils.sleep(1000); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) { + outputFieldsDeclarer.declare(new Fields("line")); + } + + + /** + * 模拟数据 + */ + private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); + } + +} +``` + +产生的模拟数据格式如下: + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +``` + + + +### 2.4 SplitBolt + +```java +/** + * 将每行数据按照指定分隔符进行拆分 + */ +public class SplitBolt extends BaseRichBolt { + + private OutputCollector collector; + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector = collector; + } + + @Override + public void execute(Tuple input) { + String line = input.getStringByField("line"); + String[] words = line.split("\t"); + for (String word : words) { + collector.emit(tuple(word, 1)); + } + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("word", "count")); + } +} +``` + +### 2.5 CountBolt + +```java +/** + * 进行词频统计 + */ +public class CountBolt extends BaseRichBolt { + + private Map counts = new HashMap<>(); + + private OutputCollector collector; + + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector=collector; + } + + @Override + public void execute(Tuple input) { + String word = input.getStringByField("word"); + Integer count = counts.get(word); + if (count == null) { + count = 0; + } + count++; + counts.put(word, count); + // 输出 + collector.emit(new Values(word, String.valueOf(count))); + + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("word", "count")); + } +} +``` + +### 2.6 WordCountToHBaseApp + +```java +/** + * 进行词频统计 并将统计结果存储到 HBase 中 + */ +public class WordCountToHBaseApp { + + private static final String DATA_SOURCE_SPOUT = "dataSourceSpout"; + private static final String SPLIT_BOLT = "splitBolt"; + private static final String COUNT_BOLT = "countBolt"; + private static final String HBASE_BOLT = "hbaseBolt"; + + public static void main(String[] args) { + + // storm 的配置 + Config config = new Config(); + + // HBase 的配置 + Map hbConf = new HashMap<>(); + hbConf.put("hbase.rootdir", "hdfs://hadoop001:8020/hbase"); + hbConf.put("hbase.zookeeper.quorum", "hadoop001:2181"); + + // 将 HBase 的配置传入 Storm 的配置中 + config.put("hbase.conf", hbConf); + + // 定义流数据与 HBase 中数据的映射 + SimpleHBaseMapper mapper = new SimpleHBaseMapper() + .withRowKeyField("word") + .withColumnFields(new Fields("word","count")) + .withColumnFamily("info"); + + /* + * 给 HBaseBolt 传入表名、数据映射关系、和 HBase 的配置信息 + * 表需要预先创建: create 'WordCount','info' + */ + HBaseBolt hbase = new HBaseBolt("WordCount", mapper) + .withConfigKey("hbase.conf"); + + // 构建 Topology + TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout(),1); + // split + builder.setBolt(SPLIT_BOLT, new SplitBolt(), 1).shuffleGrouping(DATA_SOURCE_SPOUT); + // count + builder.setBolt(COUNT_BOLT, new CountBolt(),1).shuffleGrouping(SPLIT_BOLT); + // save to HBase + builder.setBolt(HBASE_BOLT, hbase, 1).shuffleGrouping(COUNT_BOLT); + + + // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动 + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterWordCountToRedisApp", config, builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalWordCountToRedisApp", + config, builder.createTopology()); + } + } +} +``` + +### 2.7 启动测试 + +可以用直接使用本地模式运行,也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包,打包命令如下: + +```shell +# mvn clean package -D maven.test.skip=true +``` + +运行后,数据会存储到 HBase 的 `WordCount` 表中。使用以下命令查看表的内容: + +```shell +hbase > scan 'WordCount' +``` + +
+ + + +### 2.8 withCounterFields + +在上面的用例中我们是手动编码来实现词频统计,并将最后的结果存储到 HBase 中。其实也可以在构建 `SimpleHBaseMapper` 的时候通过 `withCounterFields` 指定 count 字段,被指定的字段会自动进行累加操作,这样也可以实现词频统计。需要注意的是 withCounterFields 指定的字段必须是 Long 类型,不能是 String 类型。 + +```java +SimpleHBaseMapper mapper = new SimpleHBaseMapper() + .withRowKeyField("word") + .withColumnFields(new Fields("word")) + .withCounterFields(new Fields("count")) + .withColumnFamily("cf"); +``` + + + +## 参考资料 + +1. [Apache HDFS Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hdfs.html) +2. [Apache HBase Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-hbase.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Kakfa.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Kakfa.md" new file mode 100644 index 0000000..771a824 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Kakfa.md" @@ -0,0 +1,367 @@ +# Storm集成Kafka + + + + +## 一、整合说明 + +Storm 官方对 Kafka 的整合分为两个版本,官方说明文档分别如下: + ++ [Storm Kafka Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-kafka.html) : 主要是针对 0.8.x 版本的 Kafka 提供整合支持; ++ [Storm Kafka Integration (0.10.x+)]() : 包含 Kafka 新版本的 consumer API,主要对 Kafka 0.10.x + 提供整合支持。 + +这里我服务端安装的 Kafka 版本为 2.2.0(Released Mar 22, 2019) ,按照官方 0.10.x+ 的整合文档进行整合,不适用于 0.8.x 版本的 Kafka。 + +## 二、写入数据到Kafka + +### 2.1 项目结构 + +
+ +### 2.2 项目主要依赖 + +```xml + + 1.2.2 + 2.2.0 + + + + + org.apache.storm + storm-core + ${storm.version} + + + org.apache.storm + storm-kafka-client + ${storm.version} + + + org.apache.kafka + kafka-clients + ${kafka.version} + + +``` + +### 2.3 DataSourceSpout + +```java +/** + * 产生词频样本的数据源 + */ +public class DataSourceSpout extends BaseRichSpout { + + private List list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive"); + + private SpoutOutputCollector spoutOutputCollector; + + @Override + public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) { + this.spoutOutputCollector = spoutOutputCollector; + } + + @Override + public void nextTuple() { + // 模拟产生数据 + String lineData = productData(); + spoutOutputCollector.emit(new Values(lineData)); + Utils.sleep(1000); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) { + outputFieldsDeclarer.declare(new Fields("line")); + } + + + /** + * 模拟数据 + */ + private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); + } + +} +``` + +产生的模拟数据格式如下: + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +``` + +### 2.4 WritingToKafkaApp + +```java +/** + * 写入数据到 Kafka 中 + */ +public class WritingToKafkaApp { + + private static final String BOOTSTRAP_SERVERS = "hadoop001:9092"; + private static final String TOPIC_NAME = "storm-topic"; + + public static void main(String[] args) { + + + TopologyBuilder builder = new TopologyBuilder(); + + // 定义 Kafka 生产者属性 + Properties props = new Properties(); + /* + * 指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找其他 broker 的信息。 + * 不过建议至少要提供两个 broker 的信息作为容错。 + */ + props.put("bootstrap.servers", BOOTSTRAP_SERVERS); + /* + * acks 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。 + * acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。 + * acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。 + * acks=all : 只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。 + */ + props.put("acks", "1"); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + + KafkaBolt bolt = new KafkaBolt() + .withProducerProperties(props) + .withTopicSelector(new DefaultTopicSelector(TOPIC_NAME)) + .withTupleToKafkaMapper(new FieldNameBasedTupleToKafkaMapper<>()); + + builder.setSpout("sourceSpout", new DataSourceSpout(), 1); + builder.setBolt("kafkaBolt", bolt, 1).shuffleGrouping("sourceSpout"); + + + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterWritingToKafkaApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalWritingToKafkaApp", + new Config(), builder.createTopology()); + } + } +} +``` + +### 2.5 测试准备工作 + +进行测试前需要启动 Kakfa: + +#### 1. 启动Kakfa + +Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的: + +```shell +# zookeeper启动命令 +bin/zkServer.sh start + +# 内置zookeeper启动命令 +bin/zookeeper-server-start.sh config/zookeeper.properties +``` + +启动单节点 kafka 用于测试: + +```shell +# bin/kafka-server-start.sh config/server.properties +``` + +#### 2. 创建topic + +```shell +# 创建用于测试主题 +bin/kafka-topics.sh --create --bootstrap-server hadoop001:9092 --replication-factor 1 --partitions 1 --topic storm-topic + +# 查看所有主题 + bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092 +``` + +#### 3. 启动消费者 + + 启动一个消费者用于观察写入情况,启动命令如下: + +```shell +# bin/kafka-console-consumer.sh --bootstrap-server hadoop001:9092 --topic storm-topic --from-beginning +``` + +### 2.6 测试 + +可以用直接使用本地模式运行,也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包,打包命令如下: + +```shell +# mvn clean package -D maven.test.skip=true +``` + +启动后,消费者监听情况如下: + +
+ + + +## 三、从Kafka中读取数据 + +### 3.1 项目结构 + +
+ +### 3.2 ReadingFromKafkaApp + +```java +/** + * 从 Kafka 中读取数据 + */ +public class ReadingFromKafkaApp { + + private static final String BOOTSTRAP_SERVERS = "hadoop001:9092"; + private static final String TOPIC_NAME = "storm-topic"; + + public static void main(String[] args) { + + final TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout("kafka_spout", new KafkaSpout<>(getKafkaSpoutConfig(BOOTSTRAP_SERVERS, TOPIC_NAME)), 1); + builder.setBolt("bolt", new LogConsoleBolt()).shuffleGrouping("kafka_spout"); + + // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动 + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterReadingFromKafkaApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalReadingFromKafkaApp", + new Config(), builder.createTopology()); + } + } + + private static KafkaSpoutConfig getKafkaSpoutConfig(String bootstrapServers, String topic) { + return KafkaSpoutConfig.builder(bootstrapServers, topic) + // 除了分组 ID,以下配置都是可选的。分组 ID 必须指定,否则会抛出 InvalidGroupIdException 异常 + .setProp(ConsumerConfig.GROUP_ID_CONFIG, "kafkaSpoutTestGroup") + // 定义重试策略 + .setRetry(getRetryService()) + // 定时提交偏移量的时间间隔,默认是 15s + .setOffsetCommitPeriodMs(10_000) + .build(); + } + + // 定义重试策略 + private static KafkaSpoutRetryService getRetryService() { + return new KafkaSpoutRetryExponentialBackoff(TimeInterval.microSeconds(500), + TimeInterval.milliSeconds(2), Integer.MAX_VALUE, TimeInterval.seconds(10)); + } +} + +``` + +### 3.3 LogConsoleBolt + +```java +/** + * 打印从 Kafka 中获取的数据 + */ +public class LogConsoleBolt extends BaseRichBolt { + + + private OutputCollector collector; + + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector=collector; + } + + public void execute(Tuple input) { + try { + String value = input.getStringByField("value"); + System.out.println("received from kafka : "+ value); + // 必须 ack,否则会重复消费 kafka 中的消息 + collector.ack(input); + }catch (Exception e){ + e.printStackTrace(); + collector.fail(input); + } + + } + + public void declareOutputFields(OutputFieldsDeclarer declarer) { + + } +} +``` + +这里从 `value` 字段中获取 kafka 输出的值数据。 + +在开发中,我们可以通过继承 `RecordTranslator` 接口定义了 Kafka 中 Record 与输出流之间的映射关系,可以在构建 `KafkaSpoutConfig` 的时候通过构造器或者 `setRecordTranslator()` 方法传入,并最后传递给具体的 `KafkaSpout`。 + +默认情况下使用内置的 `DefaultRecordTranslator`,其源码如下,`FIELDS` 中 定义了 tuple 中所有可用的字段:主题,分区,偏移量,消息键,值。 + +```java +public class DefaultRecordTranslator implements RecordTranslator { + private static final long serialVersionUID = -5782462870112305750L; + public static final Fields FIELDS = new Fields("topic", "partition", "offset", "key", "value"); + @Override + public List apply(ConsumerRecord record) { + return new Values(record.topic(), + record.partition(), + record.offset(), + record.key(), + record.value()); + } + + @Override + public Fields getFieldsFor(String stream) { + return FIELDS; + } + + @Override + public List streams() { + return DEFAULT_STREAM; + } +} +``` + +### 3.4 启动测试 + +这里启动一个生产者用于发送测试数据,启动命令如下: + +```shell +# bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic storm-topic +``` + +
+ +本地运行的项目接收到从 Kafka 发送过来的数据: + +
+ + + +
+ +> 用例源码下载地址:[storm-kafka-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-kafka-integration) + + + +## 参考资料 + +1. [Storm Kafka Integration (0.10.x+)](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-kafka-client.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Redis\350\257\246\350\247\243.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Redis\350\257\246\350\247\243.md" new file mode 100644 index 0000000..48c0829 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Storm\351\233\206\346\210\220Redis\350\257\246\350\247\243.md" @@ -0,0 +1,655 @@ +# Storm 集成 Redis 详解 + + + + +## 一、简介 + +Storm-Redis 提供了 Storm 与 Redis 的集成支持,你只需要引入对应的依赖即可使用: + +```xml + + org.apache.storm + storm-redis + ${storm.version} + jar + +``` + +Storm-Redis 使用 Jedis 为 Redis 客户端,并提供了如下三个基本的 Bolt 实现: + ++ **RedisLookupBolt**:从 Redis 中查询数据; ++ **RedisStoreBolt**:存储数据到 Redis; ++ **RedisFilterBolt** : 查询符合条件的数据; + +`RedisLookupBolt`、`RedisStoreBolt`、`RedisFilterBolt ` 均继承自 `AbstractRedisBolt` 抽象类。我们可以通过继承该抽象类,实现自定义 RedisBolt,进行功能的拓展。 + + + +## 二、集成案例 + +### 2.1 项目结构 + +这里首先给出一个集成案例:进行词频统计并将最后的结果存储到 Redis。项目结构如下: + +
+ +> 用例源码下载地址:[storm-redis-integration](https://github.com/heibaiying/BigData-Notes/tree/master/code/Storm/storm-redis-integration) + +### 2.2 项目依赖 + +项目主要依赖如下: + +```xml + + 1.2.2 + + + + + org.apache.storm + storm-core + ${storm.version} + + + org.apache.storm + storm-redis + ${storm.version} + + +``` + +### 2.3 DataSourceSpout + +```java +/** + * 产生词频样本的数据源 + */ +public class DataSourceSpout extends BaseRichSpout { + + private List list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive"); + + private SpoutOutputCollector spoutOutputCollector; + + @Override + public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) { + this.spoutOutputCollector = spoutOutputCollector; + } + + @Override + public void nextTuple() { + // 模拟产生数据 + String lineData = productData(); + spoutOutputCollector.emit(new Values(lineData)); + Utils.sleep(1000); + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) { + outputFieldsDeclarer.declare(new Fields("line")); + } + + + /** + * 模拟数据 + */ + private String productData() { + Collections.shuffle(list); + Random random = new Random(); + int endIndex = random.nextInt(list.size()) % (list.size()) + 1; + return StringUtils.join(list.toArray(), "\t", 0, endIndex); + } + +} +``` + +产生的模拟数据格式如下: + +```properties +Spark HBase +Hive Flink Storm Hadoop HBase Spark +Flink +HBase Storm +HBase Hadoop Hive Flink +HBase Flink Hive Storm +Hive Flink Hadoop +HBase Hive +Hadoop Spark HBase Storm +``` + +### 2.4 SplitBolt + +```java +/** + * 将每行数据按照指定分隔符进行拆分 + */ +public class SplitBolt extends BaseRichBolt { + + private OutputCollector collector; + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector = collector; + } + + @Override + public void execute(Tuple input) { + String line = input.getStringByField("line"); + String[] words = line.split("\t"); + for (String word : words) { + collector.emit(new Values(word, String.valueOf(1))); + } + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("word", "count")); + } +} +``` + +### 2.5 CountBolt + +```java +/** + * 进行词频统计 + */ +public class CountBolt extends BaseRichBolt { + + private Map counts = new HashMap<>(); + + private OutputCollector collector; + + + @Override + public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { + this.collector=collector; + } + + @Override + public void execute(Tuple input) { + String word = input.getStringByField("word"); + Integer count = counts.get(word); + if (count == null) { + count = 0; + } + count++; + counts.put(word, count); + // 输出 + collector.emit(new Values(word, String.valueOf(count))); + + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("word", "count")); + } +} +``` + +### 2.6 WordCountStoreMapper + +实现 RedisStoreMapper 接口,定义 tuple 与 Redis 中数据的映射关系:即需要指定 tuple 中的哪个字段为 key,哪个字段为 value,并且存储到 Redis 的何种数据结构中。 + +```java +/** + * 定义 tuple 与 Redis 中数据的映射关系 + */ +public class WordCountStoreMapper implements RedisStoreMapper { + private RedisDataTypeDescription description; + private final String hashKey = "wordCount"; + + public WordCountStoreMapper() { + description = new RedisDataTypeDescription( + RedisDataTypeDescription.RedisDataType.HASH, hashKey); + } + + @Override + public RedisDataTypeDescription getDataTypeDescription() { + return description; + } + + @Override + public String getKeyFromTuple(ITuple tuple) { + return tuple.getStringByField("word"); + } + + @Override + public String getValueFromTuple(ITuple tuple) { + return tuple.getStringByField("count"); + } +} +``` + +### 2.7 WordCountToRedisApp + +```java +/** + * 进行词频统计 并将统计结果存储到 Redis 中 + */ +public class WordCountToRedisApp { + + private static final String DATA_SOURCE_SPOUT = "dataSourceSpout"; + private static final String SPLIT_BOLT = "splitBolt"; + private static final String COUNT_BOLT = "countBolt"; + private static final String STORE_BOLT = "storeBolt"; + + //在实际开发中这些参数可以将通过外部传入 使得程序更加灵活 + private static final String REDIS_HOST = "192.168.200.226"; + private static final int REDIS_PORT = 6379; + + public static void main(String[] args) { + TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout()); + // split + builder.setBolt(SPLIT_BOLT, new SplitBolt()).shuffleGrouping(DATA_SOURCE_SPOUT); + // count + builder.setBolt(COUNT_BOLT, new CountBolt()).shuffleGrouping(SPLIT_BOLT); + // save to redis + JedisPoolConfig poolConfig = new JedisPoolConfig.Builder() + .setHost(REDIS_HOST).setPort(REDIS_PORT).build(); + RedisStoreMapper storeMapper = new WordCountStoreMapper(); + RedisStoreBolt storeBolt = new RedisStoreBolt(poolConfig, storeMapper); + builder.setBolt(STORE_BOLT, storeBolt).shuffleGrouping(COUNT_BOLT); + + // 如果外部传参 cluster 则代表线上环境启动否则代表本地启动 + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterWordCountToRedisApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalWordCountToRedisApp", + new Config(), builder.createTopology()); + } + } +} +``` + +### 2.8 启动测试 + +可以用直接使用本地模式运行,也可以打包后提交到服务器集群运行。本仓库提供的源码默认采用 `maven-shade-plugin` 进行打包,打包命令如下: + +```shell +# mvn clean package -D maven.test.skip=true +``` + +启动后,查看 Redis 中的数据: + +
+ + + +## 三、storm-redis 实现原理 + +### 3.1 AbstractRedisBolt + +`RedisLookupBolt`、`RedisStoreBolt`、`RedisFilterBolt ` 均继承自 `AbstractRedisBolt` 抽象类,和我们自定义实现 Bolt 一样,`AbstractRedisBolt` 间接继承自 `BaseRichBolt`。 + + + +
+ +`AbstractRedisBolt` 中比较重要的是 prepare 方法,在该方法中通过外部传入的 jedis 连接池配置 ( jedisPoolConfig/jedisClusterConfig) 创建用于管理 Jedis 实例的容器 `JedisCommandsInstanceContainer`。 + +```java +public abstract class AbstractRedisBolt extends BaseTickTupleAwareRichBolt { + protected OutputCollector collector; + + private transient JedisCommandsInstanceContainer container; + + private JedisPoolConfig jedisPoolConfig; + private JedisClusterConfig jedisClusterConfig; + + ...... + + @Override + public void prepare(Map map, TopologyContext topologyContext, OutputCollector collector) { + // FIXME: stores map (stormConf), topologyContext and expose these to derived classes + this.collector = collector; + + if (jedisPoolConfig != null) { + this.container = JedisCommandsContainerBuilder.build(jedisPoolConfig); + } else if (jedisClusterConfig != null) { + this.container = JedisCommandsContainerBuilder.build(jedisClusterConfig); + } else { + throw new IllegalArgumentException("Jedis configuration not found"); + } + } + + ....... +} +``` + +`JedisCommandsInstanceContainer` 的 `build()` 方法如下,实际上就是创建 JedisPool 或 JedisCluster 并传入容器中。 + +```java +public static JedisCommandsInstanceContainer build(JedisPoolConfig config) { + JedisPool jedisPool = new JedisPool(DEFAULT_POOL_CONFIG, config.getHost(), config.getPort(), config.getTimeout(), config.getPassword(), config.getDatabase()); + return new JedisContainer(jedisPool); + } + + public static JedisCommandsInstanceContainer build(JedisClusterConfig config) { + JedisCluster jedisCluster = new JedisCluster(config.getNodes(), config.getTimeout(), config.getTimeout(), config.getMaxRedirections(), config.getPassword(), DEFAULT_POOL_CONFIG); + return new JedisClusterContainer(jedisCluster); + } +``` + +### 3.2 RedisStoreBolt和RedisLookupBolt + +`RedisStoreBolt` 中比较重要的是 process 方法,该方法主要从 storeMapper 中获取传入 key/value 的值,并按照其存储类型 `dataType` 调用 jedisCommand 的对应方法进行存储。 + +RedisLookupBolt 的实现基本类似,从 lookupMapper 中获取传入的 key 值,并进行查询操作。 + +```java +public class RedisStoreBolt extends AbstractRedisBolt { + private final RedisStoreMapper storeMapper; + private final RedisDataTypeDescription.RedisDataType dataType; + private final String additionalKey; + + public RedisStoreBolt(JedisPoolConfig config, RedisStoreMapper storeMapper) { + super(config); + this.storeMapper = storeMapper; + + RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription(); + this.dataType = dataTypeDescription.getDataType(); + this.additionalKey = dataTypeDescription.getAdditionalKey(); + } + + public RedisStoreBolt(JedisClusterConfig config, RedisStoreMapper storeMapper) { + super(config); + this.storeMapper = storeMapper; + + RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription(); + this.dataType = dataTypeDescription.getDataType(); + this.additionalKey = dataTypeDescription.getAdditionalKey(); + } + + + @Override + public void process(Tuple input) { + String key = storeMapper.getKeyFromTuple(input); + String value = storeMapper.getValueFromTuple(input); + + JedisCommands jedisCommand = null; + try { + jedisCommand = getInstance(); + + switch (dataType) { + case STRING: + jedisCommand.set(key, value); + break; + + case LIST: + jedisCommand.rpush(key, value); + break; + + case HASH: + jedisCommand.hset(additionalKey, key, value); + break; + + case SET: + jedisCommand.sadd(key, value); + break; + + case SORTED_SET: + jedisCommand.zadd(additionalKey, Double.valueOf(value), key); + break; + + case HYPER_LOG_LOG: + jedisCommand.pfadd(key, value); + break; + + case GEO: + String[] array = value.split(":"); + if (array.length != 2) { + throw new IllegalArgumentException("value structure should be longitude:latitude"); + } + + double longitude = Double.valueOf(array[0]); + double latitude = Double.valueOf(array[1]); + jedisCommand.geoadd(additionalKey, longitude, latitude, key); + break; + + default: + throw new IllegalArgumentException("Cannot process such data type: " + dataType); + } + + collector.ack(input); + } catch (Exception e) { + this.collector.reportError(e); + this.collector.fail(input); + } finally { + returnInstance(jedisCommand); + } + } + + ......... +} + +``` + +### 3.3 JedisCommands + +JedisCommands 接口中定义了所有的 Redis 客户端命令,它有以下三个实现类,分别是 Jedis、JedisCluster、ShardedJedis。Strom 中主要使用前两种实现类,具体调用哪一个实现类来执行命令,由传入的是 jedisPoolConfig 还是 jedisClusterConfig 来决定。 + +
+ +### 3.4 RedisMapper 和 TupleMapper + +RedisMapper 和 TupleMapper 定义了 tuple 和 Redis 中的数据如何进行映射转换。 + +
+ +#### 1. TupleMapper + +TupleMapper 主要定义了两个方法: + ++ getKeyFromTuple(ITuple tuple): 从 tuple 中获取那个字段作为 Key; + ++ getValueFromTuple(ITuple tuple):从 tuple 中获取那个字段作为 Value; + +#### 2. RedisMapper + +定义了获取数据类型的方法 `getDataTypeDescription()`,RedisDataTypeDescription 中 RedisDataType 枚举类定义了所有可用的 Redis 数据类型: + +```java +public class RedisDataTypeDescription implements Serializable { + + public enum RedisDataType { STRING, HASH, LIST, SET, SORTED_SET, HYPER_LOG_LOG, GEO } + ...... + } +``` + +#### 3. RedisStoreMapper + +RedisStoreMapper 继承 TupleMapper 和 RedisMapper 接口,用于数据存储时,没有定义额外方法。 + +#### 4. RedisLookupMapper + +RedisLookupMapper 继承 TupleMapper 和 RedisMapper 接口: + ++ 定义了 declareOutputFields 方法,声明输出的字段。 ++ 定义了 toTuple 方法,将查询结果组装为 Storm 的 Values 的集合,并用于发送。 + +下面的例子表示从输入 `Tuple` 的获取 `word` 字段作为 key,使用 `RedisLookupBolt` 进行查询后,将 key 和查询结果 value 组装为 values 并发送到下一个处理单元。 + +```java +class WordCountRedisLookupMapper implements RedisLookupMapper { + private RedisDataTypeDescription description; + private final String hashKey = "wordCount"; + + public WordCountRedisLookupMapper() { + description = new RedisDataTypeDescription( + RedisDataTypeDescription.RedisDataType.HASH, hashKey); + } + + @Override + public List toTuple(ITuple input, Object value) { + String member = getKeyFromTuple(input); + List values = Lists.newArrayList(); + values.add(new Values(member, value)); + return values; + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("wordName", "count")); + } + + @Override + public RedisDataTypeDescription getDataTypeDescription() { + return description; + } + + @Override + public String getKeyFromTuple(ITuple tuple) { + return tuple.getStringByField("word"); + } + + @Override + public String getValueFromTuple(ITuple tuple) { + return null; + } +} +``` + +#### 5. RedisFilterMapper + +RedisFilterMapper 继承 TupleMapper 和 RedisMapper 接口,用于查询数据时,定义了 declareOutputFields 方法,声明输出的字段。如下面的实现: + +```java +@Override +public void declareOutputFields(OutputFieldsDeclarer declarer) { + declarer.declare(new Fields("wordName", "count")); +} + +``` + +## 四、自定义RedisBolt实现词频统计 + +### 4.1 实现原理 + +自定义 RedisBolt:主要利用 Redis 中哈希结构的 `hincrby key field` 命令进行词频统计。在 Redis 中 `hincrby` 的执行效果如下。hincrby 可以将字段按照指定的值进行递增,如果该字段不存在的话,还会新建该字段,并赋值为 0。通过这个命令可以非常轻松的实现词频统计功能。 + +```shell +redis> HSET myhash field 5 +(integer) 1 +redis> HINCRBY myhash field 1 +(integer) 6 +redis> HINCRBY myhash field -1 +(integer) 5 +redis> HINCRBY myhash field -10 +(integer) -5 +redis> +``` + +### 4.2 项目结构 + +
+ +### 4.3 自定义RedisBolt的代码实现 + +```java +/** + * 自定义 RedisBolt 利用 Redis 的哈希数据结构的 hincrby key field 命令进行词频统计 + */ +public class RedisCountStoreBolt extends AbstractRedisBolt { + + private final RedisStoreMapper storeMapper; + private final RedisDataTypeDescription.RedisDataType dataType; + private final String additionalKey; + + public RedisCountStoreBolt(JedisPoolConfig config, RedisStoreMapper storeMapper) { + super(config); + this.storeMapper = storeMapper; + RedisDataTypeDescription dataTypeDescription = storeMapper.getDataTypeDescription(); + this.dataType = dataTypeDescription.getDataType(); + this.additionalKey = dataTypeDescription.getAdditionalKey(); + } + + @Override + protected void process(Tuple tuple) { + String key = storeMapper.getKeyFromTuple(tuple); + String value = storeMapper.getValueFromTuple(tuple); + + JedisCommands jedisCommand = null; + try { + jedisCommand = getInstance(); + if (dataType == RedisDataTypeDescription.RedisDataType.HASH) { + jedisCommand.hincrBy(additionalKey, key, Long.valueOf(value)); + } else { + throw new IllegalArgumentException("Cannot process such data type for Count: " + dataType); + } + + collector.ack(tuple); + } catch (Exception e) { + this.collector.reportError(e); + this.collector.fail(tuple); + } finally { + returnInstance(jedisCommand); + } + } + + @Override + public void declareOutputFields(OutputFieldsDeclarer declarer) { + + } +} +``` + +### 4.4 CustomRedisCountApp + +```java +/** + * 利用自定义的 RedisBolt 实现词频统计 + */ +public class CustomRedisCountApp { + + private static final String DATA_SOURCE_SPOUT = "dataSourceSpout"; + private static final String SPLIT_BOLT = "splitBolt"; + private static final String STORE_BOLT = "storeBolt"; + + private static final String REDIS_HOST = "192.168.200.226"; + private static final int REDIS_PORT = 6379; + + public static void main(String[] args) { + TopologyBuilder builder = new TopologyBuilder(); + builder.setSpout(DATA_SOURCE_SPOUT, new DataSourceSpout()); + // split + builder.setBolt(SPLIT_BOLT, new SplitBolt()).shuffleGrouping(DATA_SOURCE_SPOUT); + // save to redis and count + JedisPoolConfig poolConfig = new JedisPoolConfig.Builder() + .setHost(REDIS_HOST).setPort(REDIS_PORT).build(); + RedisStoreMapper storeMapper = new WordCountStoreMapper(); + RedisCountStoreBolt countStoreBolt = new RedisCountStoreBolt(poolConfig, storeMapper); + builder.setBolt(STORE_BOLT, countStoreBolt).shuffleGrouping(SPLIT_BOLT); + + // 如果外部传参 cluster 则代表线上环境启动,否则代表本地启动 + if (args.length > 0 && args[0].equals("cluster")) { + try { + StormSubmitter.submitTopology("ClusterCustomRedisCountApp", new Config(), builder.createTopology()); + } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) { + e.printStackTrace(); + } + } else { + LocalCluster cluster = new LocalCluster(); + cluster.submitTopology("LocalCustomRedisCountApp", + new Config(), builder.createTopology()); + } + } +} +``` + + + +## 参考资料 + +1. [Storm Redis Integration](http://storm.apache.org/releases/2.0.0-SNAPSHOT/storm-redis.html) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_ACL\346\235\203\351\231\220\346\216\247\345\210\266.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_ACL\346\235\203\351\231\220\346\216\247\345\210\266.md" new file mode 100644 index 0000000..a930159 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_ACL\346\235\203\351\231\220\346\216\247\345\210\266.md" @@ -0,0 +1,283 @@ +# Zookeeper ACL + + + + +## 一、前言 + +为了避免存储在 Zookeeper 上的数据被其他程序或者人为误修改,Zookeeper 提供了 ACL(Access Control Lists) 进行权限控制。只有拥有对应权限的用户才可以对节点进行增删改查等操作。下文分别介绍使用原生的 Shell 命令和 Apache Curator 客户端进行权限设置。 + +## 二、使用Shell进行权限管理 + +### 2.1 设置与查看权限 + +想要给某个节点设置权限 (ACL),有以下两个可选的命令: + +```shell + # 1.给已有节点赋予权限 + setAcl path acl + + # 2.在创建节点时候指定权限 + create [-s] [-e] path data acl +``` + +查看指定节点的权限命令如下: + +```shell +getAcl path +``` + +### 2.2 权限组成 + +Zookeeper 的权限由[scheme : id :permissions]三部分组成,其中 Schemes 和 Permissions 内置的可选项分别如下: + +**Permissions 可选项**: + +- **CREATE**:允许创建子节点; +- **READ**:允许从节点获取数据并列出其子节点; +- **WRITE**:允许为节点设置数据; +- **DELETE**:允许删除子节点; +- **ADMIN**:允许为节点设置权限。 + +**Schemes 可选项**: + +- **world**:默认模式,所有客户端都拥有指定的权限。world 下只有一个 id 选项,就是 anyone,通常组合写法为 `world:anyone:[permissons]`; +- **auth**:只有经过认证的用户才拥有指定的权限。通常组合写法为 `auth:user:password:[permissons]`,使用这种模式时,你需要先进行登录,之后采用 auth 模式设置权限时,`user` 和 `password` 都将使用登录的用户名和密码; +- **digest**:只有经过认证的用户才拥有指定的权限。通常组合写法为 `auth:user:BASE64(SHA1(password)):[permissons]`,这种形式下的密码必须通过 SHA1 和 BASE64 进行双重加密; +- **ip**:限制只有特定 IP 的客户端才拥有指定的权限。通常组成写法为 `ip:182.168.0.168:[permissions]`; +- **super**:代表超级管理员,拥有所有的权限,需要修改 Zookeeper 启动脚本进行配置。 + + + +### 2.3 添加认证信息 + +可以使用如下所示的命令为当前 Session 添加用户认证信息,等价于登录操作。 + +```shell +# 格式 +addauth scheme auth + +#示例:添加用户名为heibai,密码为root的用户认证信息 +addauth digest heibai:root +``` + + + +### 2.4 权限设置示例 + +#### 1. world模式 + +world 是一种默认的模式,即创建时如果不指定权限,则默认的权限就是 world。 + +```shell +[zk: localhost:2181(CONNECTED) 32] create /hadoop 123 +Created /hadoop +[zk: localhost:2181(CONNECTED) 33] getAcl /hadoop +'world,'anyone #默认的权限 +: cdrwa +[zk: localhost:2181(CONNECTED) 34] setAcl /hadoop world:anyone:cwda # 修改节点,不允许所有客户端读 +.... +[zk: localhost:2181(CONNECTED) 35] get /hadoop +Authentication is not valid : /hadoop # 权限不足 + +``` + +#### 2. auth模式 + +```shell +[zk: localhost:2181(CONNECTED) 36] addauth digest heibai:heibai # 登录 +[zk: localhost:2181(CONNECTED) 37] setAcl /hadoop auth::cdrwa # 设置权限 +[zk: localhost:2181(CONNECTED) 38] getAcl /hadoop # 获取权限 +'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s= #用户名和密码 (密码经过加密处理),注意返回的权限类型是 digest +: cdrwa + +#用户名和密码都是使用登录的用户名和密码,即使你在创建权限时候进行指定也是无效的 +[zk: localhost:2181(CONNECTED) 39] setAcl /hadoop auth:root:root:cdrwa #指定用户名和密码为 root +[zk: localhost:2181(CONNECTED) 40] getAcl /hadoop +'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s= #无效,使用的用户名和密码依然还是 heibai +: cdrwa + +``` + +#### 3. digest模式 + +```shell +[zk:44] create /spark "spark" digest:heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=:cdrwa #指定用户名和加密后的密码 +[zk:45] getAcl /spark #获取权限 +'digest,'heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s= # 返回的权限类型是 digest +: cdrwa +``` + +到这里你可以发现使用 `auth` 模式设置的权限和使用 `digest` 模式设置的权限,在最终结果上,得到的权限模式都是 `digest`。某种程度上,你可以把 `auth` 模式理解成是 `digest` 模式的一种简便实现。因为在 `digest` 模式下,每次设置都需要书写用户名和加密后的密码,这是比较繁琐的,采用 `auth` 模式就可以避免这种麻烦。 + +#### 4. ip模式 + +限定只有特定的 ip 才能访问。 + +```shell +[zk: localhost:2181(CONNECTED) 46] create /hive "hive" ip:192.168.0.108:cdrwa +[zk: localhost:2181(CONNECTED) 47] get /hive +Authentication is not valid : /hive # 当前主机已经不能访问 +``` + +这里可以看到当前主机已经不能访问,想要能够再次访问,可以使用对应 IP 的客户端,或使用下面介绍的 `super` 模式。 + +#### 5. super模式 + +需要修改启动脚本 `zkServer.sh`,并在指定位置添加超级管理员账户和密码信息: + +```shell +"-Dzookeeper.DigestAuthenticationProvider.superDigest=heibai:sCxtVJ1gPG8UW/jzFHR0A1ZKY5s=" +``` + +
+ +修改完成后需要使用 `zkServer.sh restart` 重启服务,此时再次访问限制 IP 的节点: + +```shell +[zk: localhost:2181(CONNECTED) 0] get /hive #访问受限 +Authentication is not valid : /hive +[zk: localhost:2181(CONNECTED) 1] addauth digest heibai:heibai # 登录 (添加认证信息) +[zk: localhost:2181(CONNECTED) 2] get /hive #成功访问 +hive +cZxid = 0x158 +ctime = Sat May 25 09:11:29 CST 2019 +mZxid = 0x158 +mtime = Sat May 25 09:11:29 CST 2019 +pZxid = 0x158 +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 4 +numChildren = 0 +``` + +## 三、使用Java客户端进行权限管理 + +### 3.1 主要依赖 + +这里以 Apache Curator 为例,使用前需要导入相关依赖,完整依赖如下: + +```xml + + + + org.apache.curator + curator-framework + 4.0.0 + + + org.apache.curator + curator-recipes + 4.0.0 + + + org.apache.zookeeper + zookeeper + 3.4.13 + + + + junit + junit + 4.12 + + +``` + +### 3.2 权限管理API + + Apache Curator 权限设置的示例如下: + +```java +public class AclOperation { + + private CuratorFramework client = null; + private static final String zkServerPath = "192.168.0.226:2181"; + private static final String nodePath = "/hadoop/hdfs"; + + @Before + public void prepare() { + RetryPolicy retryPolicy = new RetryNTimes(3, 5000); + client = CuratorFrameworkFactory.builder() + .authorization("digest", "heibai:123456".getBytes()) //等价于 addauth 命令 + .connectString(zkServerPath) + .sessionTimeoutMs(10000).retryPolicy(retryPolicy) + .namespace("workspace").build(); + client.start(); + } + + /** + * 新建节点并赋予权限 + */ + @Test + public void createNodesWithAcl() throws Exception { + List aclList = new ArrayList<>(); + // 对密码进行加密 + String digest1 = DigestAuthenticationProvider.generateDigest("heibai:123456"); + String digest2 = DigestAuthenticationProvider.generateDigest("ying:123456"); + Id user01 = new Id("digest", digest1); + Id user02 = new Id("digest", digest2); + // 指定所有权限 + aclList.add(new ACL(Perms.ALL, user01)); + // 如果想要指定权限的组合,中间需要使用 | ,这里的|代表的是位运算中的 按位或 + aclList.add(new ACL(Perms.DELETE | Perms.CREATE, user02)); + + // 创建节点 + byte[] data = "abc".getBytes(); + client.create().creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT) + .withACL(aclList, true) + .forPath(nodePath, data); + } + + + /** + * 给已有节点设置权限,注意这会删除所有原来节点上已有的权限设置 + */ + @Test + public void SetAcl() throws Exception { + String digest = DigestAuthenticationProvider.generateDigest("admin:admin"); + Id user = new Id("digest", digest); + client.setACL() + .withACL(Collections.singletonList(new ACL(Perms.READ | Perms.DELETE, user))) + .forPath(nodePath); + } + + /** + * 获取权限 + */ + @Test + public void getAcl() throws Exception { + List aclList = client.getACL().forPath(nodePath); + ACL acl = aclList.get(0); + System.out.println(acl.getId().getId() + + "是否有删读权限:" + (acl.getPerms() == (Perms.READ | Perms.DELETE))); + } + + @After + public void destroy() { + if (client != null) { + client.close(); + } + } +} +``` + +> 完整源码见本仓库: https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_Java\345\256\242\346\210\267\347\253\257Curator.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_Java\345\256\242\346\210\267\347\253\257Curator.md" new file mode 100644 index 0000000..76bdd58 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper_Java\345\256\242\346\210\267\347\253\257Curator.md" @@ -0,0 +1,336 @@ +# Zookeeper Java 客户端 ——Apache Curator + + + +## 一、基本依赖 + +Curator 是 Netflix 公司开源的一个 Zookeeper 客户端,目前由 Apache 进行维护。与 Zookeeper 原生客户端相比,Curator 的抽象层次更高,功能也更加丰富,是目前 Zookeeper 使用范围最广的 Java 客户端。本篇文章主要讲解其基本使用,项目采用 Maven 构建,以单元测试的方法进行讲解,相关依赖如下: + +```xml + + + + org.apache.curator + curator-framework + 4.0.0 + + + org.apache.curator + curator-recipes + 4.0.0 + + + org.apache.zookeeper + zookeeper + 3.4.13 + + + + junit + junit + 4.12 + + +``` + +> 完整源码见本仓库: https://github.com/heibaiying/BigData-Notes/tree/master/code/Zookeeper/curator + + + +## 二、客户端相关操作 + +### 2.1 创建客户端实例 + +这里使用 `@Before` 在单元测试执行前创建客户端实例,并使用 `@After` 在单元测试后关闭客户端连接。 + +```java +public class BasicOperation { + + private CuratorFramework client = null; + private static final String zkServerPath = "192.168.0.226:2181"; + private static final String nodePath = "/hadoop/yarn"; + + @Before + public void prepare() { + // 重试策略 + RetryPolicy retryPolicy = new RetryNTimes(3, 5000); + client = CuratorFrameworkFactory.builder() + .connectString(zkServerPath) + .sessionTimeoutMs(10000).retryPolicy(retryPolicy) + .namespace("workspace").build(); //指定命名空间后,client 的所有路径操作都会以/workspace 开头 + client.start(); + } + + @After + public void destroy() { + if (client != null) { + client.close(); + } + } +} +``` + +### 2.2 重试策略 + +在连接 Zookeeper 时,Curator 提供了多种重试策略以满足各种需求,所有重试策略均继承自 `RetryPolicy` 接口,如下图: + +
+ +这些重试策略类主要分为以下两类: + ++ **RetryForever** :代表一直重试,直到连接成功; ++ **SleepingRetry** : 基于一定间隔时间的重试。这里以其子类 `ExponentialBackoffRetry` 为例说明,其构造器如下: + +```java +/** + * @param baseSleepTimeMs 重试之间等待的初始时间 + * @param maxRetries 最大重试次数 + * @param maxSleepMs 每次重试间隔的最长睡眠时间(毫秒) + */ +ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs) +``` +### 2.3 判断服务状态 + +```scala +@Test +public void getStatus() { + CuratorFrameworkState state = client.getState(); + System.out.println("服务是否已经启动:" + (state == CuratorFrameworkState.STARTED)); +} +``` + + + +## 三、节点增删改查 + +### 3.1 创建节点 + +```java +@Test +public void createNodes() throws Exception { + byte[] data = "abc".getBytes(); + client.create().creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT) //节点类型 + .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE) + .forPath(nodePath, data); +} +``` + +创建时可以指定节点类型,这里的节点类型和 Zookeeper 原生的一致,全部类型定义在枚举类 `CreateMode` 中: + +```java +public enum CreateMode { + // 永久节点 + PERSISTENT (0, false, false), + //永久有序节点 + PERSISTENT_SEQUENTIAL (2, false, true), + // 临时节点 + EPHEMERAL (1, true, false), + // 临时有序节点 + EPHEMERAL_SEQUENTIAL (3, true, true); + .... +} +``` + +### 2.2 获取节点信息 + +```scala +@Test +public void getNode() throws Exception { + Stat stat = new Stat(); + byte[] data = client.getData().storingStatIn(stat).forPath(nodePath); + System.out.println("节点数据:" + new String(data)); + System.out.println("节点信息:" + stat.toString()); +} +``` + +如上所示,节点信息被封装在 `Stat` 类中,其主要属性如下: + +```java +public class Stat implements Record { + private long czxid; + private long mzxid; + private long ctime; + private long mtime; + private int version; + private int cversion; + private int aversion; + private long ephemeralOwner; + private int dataLength; + private int numChildren; + private long pzxid; + ... +} +``` + +每个属性的含义如下: + +| **状态属性** | **说明** | +| -------------- | ------------------------------------------------------------ | +| czxid | 数据节点创建时的事务 ID | +| ctime | 数据节点创建时的时间 | +| mzxid | 数据节点最后一次更新时的事务 ID | +| mtime | 数据节点最后一次更新时的时间 | +| pzxid | 数据节点的子节点最后一次被修改时的事务 ID | +| cversion | 子节点的更改次数 | +| version | 节点数据的更改次数 | +| aversion | 节点的 ACL 的更改次数 | +| ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的 SessionID;如果节点是持久节点,则该属性值为 0 | +| dataLength | 数据内容的长度 | +| numChildren | 数据节点当前的子节点个数 | + +### 2.3 获取子节点列表 + +```java +@Test +public void getChildrenNodes() throws Exception { + List childNodes = client.getChildren().forPath("/hadoop"); + for (String s : childNodes) { + System.out.println(s); + } +} +``` + +### 2.4 更新节点 + +更新时可以传入版本号也可以不传入,如果传入则类似于乐观锁机制,只有在版本号正确的时候才会被更新。 + +```scala +@Test +public void updateNode() throws Exception { + byte[] newData = "defg".getBytes(); + client.setData().withVersion(0) // 传入版本号,如果版本号错误则拒绝更新操作,并抛出 BadVersion 异常 + .forPath(nodePath, newData); +} +``` + +### 2.5 删除节点 + +```java +@Test +public void deleteNodes() throws Exception { + client.delete() + .guaranteed() // 如果删除失败,那么在会继续执行,直到成功 + .deletingChildrenIfNeeded() // 如果有子节点,则递归删除 + .withVersion(0) // 传入版本号,如果版本号错误则拒绝删除操作,并抛出 BadVersion 异常 + .forPath(nodePath); +} +``` + +### 2.6 判断节点是否存在 + +```java +@Test +public void existNode() throws Exception { + // 如果节点存在则返回其状态信息如果不存在则为 null + Stat stat = client.checkExists().forPath(nodePath + "aa/bb/cc"); + System.out.println("节点是否存在:" + !(stat == null)); +} +``` + + + +## 三、监听事件 + +### 3.1 创建一次性监听 + +和 Zookeeper 原生监听一样,使用 `usingWatcher` 注册的监听是一次性的,即监听只会触发一次,触发后就销毁。示例如下: + +```java +@Test +public void DisposableWatch() throws Exception { + client.getData().usingWatcher(new CuratorWatcher() { + public void process(WatchedEvent event) { + System.out.println("节点" + event.getPath() + "发生了事件:" + event.getType()); + } + }).forPath(nodePath); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} +``` + +### 3.2 创建永久监听 + +Curator 还提供了创建永久监听的 API,其使用方式如下: + +```java +@Test +public void permanentWatch() throws Exception { + // 使用 NodeCache 包装节点,对其注册的监听作用于节点,且是永久性的 + NodeCache nodeCache = new NodeCache(client, nodePath); + // 通常设置为 true, 代表创建 nodeCache 时,就去获取对应节点的值并缓存 + nodeCache.start(true); + nodeCache.getListenable().addListener(new NodeCacheListener() { + public void nodeChanged() { + ChildData currentData = nodeCache.getCurrentData(); + if (currentData != null) { + System.out.println("节点路径:" + currentData.getPath() + + "数据:" + new String(currentData.getData())); + } + } + }); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} +``` + +### 3.3 监听子节点 + +这里以监听 `/hadoop` 下所有子节点为例,实现方式如下: + +```scala +@Test +public void permanentChildrenNodesWatch() throws Exception { + + // 第三个参数代表除了节点状态外,是否还缓存节点内容 + PathChildrenCache childrenCache = new PathChildrenCache(client, "/hadoop", true); + /* + * StartMode 代表初始化方式: + * NORMAL: 异步初始化 + * BUILD_INITIAL_CACHE: 同步初始化 + * POST_INITIALIZED_EVENT: 异步并通知,初始化之后会触发 INITIALIZED 事件 + */ + childrenCache.start(StartMode.POST_INITIALIZED_EVENT); + + List childDataList = childrenCache.getCurrentData(); + System.out.println("当前数据节点的子节点列表:"); + childDataList.forEach(x -> System.out.println(x.getPath())); + + childrenCache.getListenable().addListener(new PathChildrenCacheListener() { + public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) { + switch (event.getType()) { + case INITIALIZED: + System.out.println("childrenCache 初始化完成"); + break; + case CHILD_ADDED: + // 需要注意的是: 即使是之前已经存在的子节点,也会触发该监听,因为会把该子节点加入 childrenCache 缓存中 + System.out.println("增加子节点:" + event.getData().getPath()); + break; + case CHILD_REMOVED: + System.out.println("删除子节点:" + event.getData().getPath()); + break; + case CHILD_UPDATED: + System.out.println("被修改的子节点的路径:" + event.getData().getPath()); + System.out.println("修改后的数据:" + new String(event.getData().getData())); + break; + } + } + }); + Thread.sleep(1000 * 1000); //休眠以观察测试效果 +} +``` diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" new file mode 100644 index 0000000..2358117 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\345\270\270\347\224\250Shell\345\221\275\344\273\244.md" @@ -0,0 +1,265 @@ +# Zookeeper常用Shell命令 + + + + +## 一、节点增删改查 + +### 1.1 启动服务和连接服务 + +```shell +# 启动服务 +bin/zkServer.sh start + +#连接服务 不指定服务地址则默认连接到localhost:2181 +zkCli.sh -server hadoop001:2181 +``` + +### 1.2 help命令 + +使用 `help` 可以查看所有命令及格式。 + +### 1.3 查看节点列表 + +查看节点列表有 `ls path` 和 `ls2 path` 两个命令,后者是前者的增强,不仅可以查看指定路径下的所有节点,还可以查看当前节点的信息。 + +```shell +[zk: localhost:2181(CONNECTED) 0] ls / +[cluster, controller_epoch, brokers, storm, zookeeper, admin, ...] +[zk: localhost:2181(CONNECTED) 1] ls2 / +[cluster, controller_epoch, brokers, storm, zookeeper, admin, ....] +cZxid = 0x0 +ctime = Thu Jan 01 08:00:00 CST 1970 +mZxid = 0x0 +mtime = Thu Jan 01 08:00:00 CST 1970 +pZxid = 0x130 +cversion = 19 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 0 +numChildren = 11 +``` + +### 1.4 新增节点 + +```shell +create [-s] [-e] path data acl #其中-s 为有序节点,-e 临时节点 +``` + +创建节点并写入数据: + +```shell +create /hadoop 123456 +``` + +创建有序节点,此时创建的节点名为指定节点名 + 自增序号: + +```shell +[zk: localhost:2181(CONNECTED) 23] create -s /a "aaa" +Created /a0000000022 +[zk: localhost:2181(CONNECTED) 24] create -s /b "bbb" +Created /b0000000023 +[zk: localhost:2181(CONNECTED) 25] create -s /c "ccc" +Created /c0000000024 +``` + +创建临时节点,临时节点会在会话过期后被删除: + +```shell +[zk: localhost:2181(CONNECTED) 26] create -e /tmp "tmp" +Created /tmp +``` + +### 1.5 查看节点 + +#### 1. 获取节点数据 + +```shell +# 格式 +get path [watch] +``` + +```shell +[zk: localhost:2181(CONNECTED) 31] get /hadoop +123456 #节点数据 +cZxid = 0x14b +ctime = Fri May 24 17:03:06 CST 2019 +mZxid = 0x14b +mtime = Fri May 24 17:03:06 CST 2019 +pZxid = 0x14b +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 6 +numChildren = 0 +``` + +节点各个属性如下表。其中一个重要的概念是 Zxid(ZooKeeper Transaction Id),ZooKeeper 节点的每一次更改都具有唯一的 Zxid,如果 Zxid1 小于 Zxid2,则 Zxid1 的更改发生在 Zxid2 更改之前。 + +| **状态属性** | **说明** | +| -------------- | ------------------------------------------------------------ | +| cZxid | 数据节点创建时的事务 ID | +| ctime | 数据节点创建时的时间 | +| mZxid | 数据节点最后一次更新时的事务 ID | +| mtime | 数据节点最后一次更新时的时间 | +| pZxid | 数据节点的子节点最后一次被修改时的事务 ID | +| cversion | 子节点的更改次数 | +| dataVersion | 节点数据的更改次数 | +| aclVersion | 节点的 ACL 的更改次数 | +| ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的 SessionID;如果节点是持久节点,则该属性值为 0 | +| dataLength | 数据内容的长度 | +| numChildren | 数据节点当前的子节点个数 | + +#### 2. 查看节点状态 + +可以使用 `stat` 命令查看节点状态,它的返回值和 `get` 命令类似,但不会返回节点数据。 + +```shell +[zk: localhost:2181(CONNECTED) 32] stat /hadoop +cZxid = 0x14b +ctime = Fri May 24 17:03:06 CST 2019 +mZxid = 0x14b +mtime = Fri May 24 17:03:06 CST 2019 +pZxid = 0x14b +cversion = 0 +dataVersion = 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 6 +numChildren = 0 +``` + +### 1.6 更新节点 + +更新节点的命令是 `set`,可以直接进行修改,如下: + +```shell +[zk: localhost:2181(CONNECTED) 33] set /hadoop 345 +cZxid = 0x14b +ctime = Fri May 24 17:03:06 CST 2019 +mZxid = 0x14c +mtime = Fri May 24 17:13:05 CST 2019 +pZxid = 0x14b +cversion = 0 +dataVersion = 1 # 注意更改后此时版本号为 1,默认创建时为 0 +aclVersion = 0 +ephemeralOwner = 0x0 +dataLength = 3 +numChildren = 0 +``` + +也可以基于版本号进行更改,此时类似于乐观锁机制,当你传入的数据版本号 (dataVersion) 和当前节点的数据版本号不符合时,zookeeper 会拒绝本次修改: + +```shell +[zk: localhost:2181(CONNECTED) 34] set /hadoop 678 0 +version No is not valid : /hadoop #无效的版本号 +``` + +### 1.7 删除节点 + +删除节点的语法如下: + +```shell +delete path [version] +``` + +和更新节点数据一样,也可以传入版本号,当你传入的数据版本号 (dataVersion) 和当前节点的数据版本号不符合时,zookeeper 不会执行删除操作。 + +```shell +[zk: localhost:2181(CONNECTED) 36] delete /hadoop 0 +version No is not valid : /hadoop #无效的版本号 +[zk: localhost:2181(CONNECTED) 37] delete /hadoop 1 +[zk: localhost:2181(CONNECTED) 38] +``` + +要想删除某个节点及其所有后代节点,可以使用递归删除,命令为 `rmr path`。 + +## 二、监听器 + +### 2.1 get path [watch] + +使用 `get path [watch]` 注册的监听器能够在节点内容发生改变的时候,向客户端发出通知。需要注意的是 zookeeper 的触发器是一次性的 (One-time trigger),即触发一次后就会立即失效。 + +```shell +[zk: localhost:2181(CONNECTED) 4] get /hadoop watch +[zk: localhost:2181(CONNECTED) 5] set /hadoop 45678 +WATCHER:: +WatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop #节点值改变 +``` + +### 2.2 stat path [watch] + +使用 `stat path [watch]` 注册的监听器能够在节点状态发生改变的时候,向客户端发出通知。 + +```shell +[zk: localhost:2181(CONNECTED) 7] stat /hadoop watch +[zk: localhost:2181(CONNECTED) 8] set /hadoop 112233 +WATCHER:: +WatchedEvent state:SyncConnected type:NodeDataChanged path:/hadoop #节点值改变 +``` + +### 2.3 ls\ls2 path [watch] + +使用 `ls path [watch]` 或 `ls2 path [watch]` 注册的监听器能够监听该节点下所有**子节点**的增加和删除操作。 + +```shell +[zk: localhost:2181(CONNECTED) 9] ls /hadoop watch +[] +[zk: localhost:2181(CONNECTED) 10] create /hadoop/yarn "aaa" +WATCHER:: +WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hadoop +``` + + + +## 三、 zookeeper 四字命令 + +| 命令 | 功能描述 | +| ---- | ------------------------------------------------------------ | +| conf | 打印服务配置的详细信息。 | +| cons | 列出连接到此服务器的所有客户端的完整连接/会话详细信息。包括接收/发送的数据包数量,会话 ID,操作延迟,上次执行的操作等信息。 | +| dump | 列出未完成的会话和临时节点。这只适用于 Leader 节点。 | +| envi | 打印服务环境的详细信息。 | +| ruok | 测试服务是否处于正确状态。如果正确则返回“imok”,否则不做任何相应。 | +| stat | 列出服务器和连接客户端的简要详细信息。 | +| wchs | 列出所有 watch 的简单信息。 | +| wchc | 按会话列出服务器 watch 的详细信息。 | +| wchp | 按路径列出服务器 watch 的详细信息。 | + +> 更多四字命令可以参阅官方文档:https://zookeeper.apache.org/doc/current/zookeeperAdmin.html + +使用前需要使用 `yum install nc` 安装 nc 命令,使用示例如下: + +```shell +[root@hadoop001 bin]# echo stat | nc localhost 2181 +Zookeeper version: 3.4.13-2d71af4dbe22557fda74f9a9b4309b15a7487f03, +built on 06/29/2018 04:05 GMT +Clients: + /0:0:0:0:0:0:0:1:50584[1](queued=0,recved=371,sent=371) + /0:0:0:0:0:0:0:1:50656[0](queued=0,recved=1,sent=0) +Latency min/avg/max: 0/0/19 +Received: 372 +Sent: 371 +Connections: 2 +Outstanding: 0 +Zxid: 0x150 +Mode: standalone +Node count: 167 +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" new file mode 100644 index 0000000..759672b --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/Zookeeper\347\256\200\344\273\213\345\217\212\346\240\270\345\277\203\346\246\202\345\277\265.md" @@ -0,0 +1,207 @@ +# Zookeeper简介及核心概念 + + + + +## 一、Zookeeper简介 + +Zookeeper 是一个开源的分布式协调服务,目前由 Apache 进行维护。Zookeeper 可以用于实现分布式系统中常见的发布/订阅、负载均衡、命令服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。它具有以下特性: + ++ **顺序一致性**:从一个客户端发起的事务请求,最终都会严格按照其发起顺序被应用到 Zookeeper 中; ++ **原子性**:所有事务请求的处理结果在整个集群中所有机器上都是一致的;不存在部分机器应用了该事务,而另一部分没有应用的情况; ++ **单一视图**:所有客户端看到的服务端数据模型都是一致的; ++ **可靠性**:一旦服务端成功应用了一个事务,则其引起的改变会一直保留,直到被另外一个事务所更改; ++ **实时性**:一旦一个事务被成功应用后,Zookeeper 可以保证客户端立即可以读取到这个事务变更后的最新状态的数据。 + + + +## 二、Zookeeper设计目标 + +Zookeeper 致力于为那些高吞吐的大型分布式系统提供一个高性能、高可用、且具有严格顺序访问控制能力的分布式协调服务。它具有以下四个目标: + +### 2.1 目标一:简单的数据模型 + +Zookeeper 通过树形结构来存储数据,它由一系列被称为 ZNode 的数据节点组成,类似于常见的文件系统。不过和常见的文件系统不同,Zookeeper 将数据全量存储在内存中,以此来实现高吞吐,减少访问延迟。 + +
+ +### 2.2 目标二:构建集群 + +可以由一组 Zookeeper 服务构成 Zookeeper 集群,集群中每台机器都会单独在内存中维护自身的状态,并且每台机器之间都保持着通讯,只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务。 + +
+ +### 2.3 目标三:顺序访问 + +对于来自客户端的每个更新请求,Zookeeper 都会分配一个全局唯一的递增 ID,这个 ID 反映了所有事务请求的先后顺序。 + +### 2.4 目标四:高性能高可用 + +ZooKeeper 将数据存全量储在内存中以保持高性能,并通过服务集群来实现高可用,由于 Zookeeper 的所有更新和删除都是基于事务的,所以其在读多写少的应用场景中有着很高的性能表现。 + + + +## 三、核心概念 + +### 3.1 集群角色 + +Zookeeper 集群中的机器分为以下三种角色: + ++ **Leader** :为客户端提供读写服务,并维护集群状态,它是由集群选举所产生的; ++ **Follower** :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态。同时也参与写操作“过半写成功”的策略和 Leader 的选举; ++ **Observer** :为客户端提供读写服务,并定期向 Leader 汇报自己的节点状态,但不参与写操作“过半写成功”的策略和 Leader 的选举,因此 Observer 可以在不影响写性能的情况下提升集群的读性能。 + +### 3.2 会话 + +Zookeeper 客户端通过 TCP 长连接连接到服务集群,会话 (Session) 从第一次连接开始就已经建立,之后通过心跳检测机制来保持有效的会话状态。通过这个连接,客户端可以发送请求并接收响应,同时也可以接收到 Watch 事件的通知。 + +关于会话中另外一个核心的概念是 sessionTimeOut(会话超时时间),当由于网络故障或者客户端主动断开等原因,导致连接断开,此时只要在会话超时时间之内重新建立连接,则之前创建的会话依然有效。 + +### 3.3 数据节点 + +Zookeeper 数据模型是由一系列基本数据单元 `Znode`(数据节点) 组成的节点树,其中根节点为 `/`。每个节点上都会保存自己的数据和节点信息。Zookeeper 中节点可以分为两大类: + ++ **持久节点** :节点一旦创建,除非被主动删除,否则一直存在; ++ **临时节点** :一旦创建该节点的客户端会话失效,则所有该客户端创建的临时节点都会被删除。 + +临时节点和持久节点都可以添加一个特殊的属性:`SEQUENTIAL`,代表该节点是否具有递增属性。如果指定该属性,那么在这个节点创建时,Zookeeper 会自动在其节点名称后面追加一个由父节点维护的递增数字。 + +### 3.4 节点信息 + +每个 ZNode 节点在存储数据的同时,都会维护一个叫做 `Stat` 的数据结构,里面存储了关于该节点的全部状态信息。如下: + +| **状态属性** | **说明** | +| -------------- | ------------------------------------------------------------ | +| czxid | 数据节点创建时的事务 ID | +| ctime | 数据节点创建时的时间 | +| mzxid | 数据节点最后一次更新时的事务 ID | +| mtime | 数据节点最后一次更新时的时间 | +| pzxid | 数据节点的子节点最后一次被修改时的事务 ID | +| cversion | 子节点的更改次数 | +| version | 节点数据的更改次数 | +| aversion | 节点的 ACL 的更改次数 | +| ephemeralOwner | 如果节点是临时节点,则表示创建该节点的会话的 SessionID;如果节点是持久节点,则该属性值为 0 | +| dataLength | 数据内容的长度 | +| numChildren | 数据节点当前的子节点个数 | + +### 3.5 Watcher + +Zookeeper 中一个常用的功能是 Watcher(事件监听器),它允许用户在指定节点上针对感兴趣的事件注册监听,当事件发生时,监听器会被触发,并将事件信息推送到客户端。该机制是 Zookeeper 实现分布式协调服务的重要特性。 + +### 3.6 ACL + +Zookeeper 采用 ACL(Access Control Lists) 策略来进行权限控制,类似于 UNIX 文件系统的权限控制。它定义了如下五种权限: + +- **CREATE**:允许创建子节点; +- **READ**:允许从节点获取数据并列出其子节点; +- **WRITE**:允许为节点设置数据; +- **DELETE**:允许删除子节点; +- **ADMIN**:允许为节点设置权限。 + + + +## 四、ZAB协议 + +### 4.1 ZAB协议与数据一致性 + +ZAB 协议是 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。通过该协议,Zookeepe 基于主从模式的系统架构来保持集群中各个副本之间数据的一致性。具体如下: + +Zookeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用原子广播协议将数据状态的变更以事务 Proposal 的形式广播到所有的副本进程上去。如下图: + +
+ +具体流程如下: + +所有的事务请求必须由唯一的 Leader 服务来处理,Leader 服务将事务请求转换为事务 Proposal,并将该 Proposal 分发给集群中所有的 Follower 服务。如果有半数的 Follower 服务进行了正确的反馈,那么 Leader 就会再次向所有的 Follower 发出 Commit 消息,要求将前一个 Proposal 进行提交。 + +### 4.2 ZAB协议的内容 + +ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播: + +#### 1. 崩溃恢复 + +当整个服务框架在启动过程中,或者当 Leader 服务器出现异常时,ZAB 协议就会进入恢复模式,通过过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出恢复模式,进入消息广播模式。 + +#### 2. 消息广播 + +ZAB 协议的消息广播过程使用的是原子广播协议。在整个消息的广播过程中,Leader 服务器会每个事物请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。具体过程如下: + +Leader 服务会为每一个 Follower 服务器分配一个单独的队列,然后将事务 Proposal 依次放入队列中,并根据 FIFO(先进先出) 的策略进行消息发送。Follower 服务在接收到 Proposal 后,会将其以事务日志的形式写入本地磁盘中,并在写入成功后反馈给 Leader 一个 Ack 响应。当 Leader 接收到超过半数 Follower 的 Ack 响应后,就会广播一个 Commit 消息给所有的 Follower 以通知其进行事务提交,之后 Leader 自身也会完成对事务的提交。而每一个 Follower 则在接收到 Commit 消息后,完成事务的提交。 + +
+ + + +## 五、Zookeeper的典型应用场景 + +### 5.1数据的发布/订阅 + +数据的发布/订阅系统,通常也用作配置中心。在分布式系统中,你可能有成千上万个服务节点,如果想要对所有服务的某项配置进行更改,由于数据节点过多,你不可逐台进行修改,而应该在设计时采用统一的配置中心。之后发布者只需要将新的配置发送到配置中心,所有服务节点即可自动下载并进行更新,从而实现配置的集中管理和动态更新。 + +Zookeeper 通过 Watcher 机制可以实现数据的发布和订阅。分布式系统的所有的服务节点可以对某个 ZNode 注册监听,之后只需要将新的配置写入该 ZNode,所有服务节点都会收到该事件。 + +### 5.2 命名服务 + +在分布式系统中,通常需要一个全局唯一的名字,如生成全局唯一的订单号等,Zookeeper 可以通过顺序节点的特性来生成全局唯一 ID,从而可以对分布式系统提供命名服务。 + +### 5.3 Master选举 + +分布式系统一个重要的模式就是主从模式 (Master/Salves),Zookeeper 可以用于该模式下的 Matser 选举。可以让所有服务节点去竞争性地创建同一个 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,这样该服务节点就可以成为 Master 节点。 + +### 5.4 分布式锁 + +可以通过 Zookeeper 的临时节点和 Watcher 机制来实现分布式锁,这里以排它锁为例进行说明: + +分布式系统的所有服务节点可以竞争性地去创建同一个临时 ZNode,由于 Zookeeper 不能有路径相同的 ZNode,必然只有一个服务节点能够创建成功,此时可以认为该节点获得了锁。其他没有获得锁的服务节点通过在该 ZNode 上注册监听,从而当锁释放时再去竞争获得锁。锁的释放情况有以下两种: + ++ 当正常执行完业务逻辑后,客户端主动将临时 ZNode 删除,此时锁被释放; ++ 当获得锁的客户端发生宕机时,临时 ZNode 会被自动删除,此时认为锁已经释放。 + +当锁被释放后,其他服务节点则再次去竞争性地进行创建,但每次都只有一个服务节点能够获取到锁,这就是排他锁。 + +### 5.5 集群管理 + +Zookeeper 还能解决大多数分布式系统中的问题: + ++ 如可以通过创建临时节点来建立心跳检测机制。如果分布式系统的某个服务节点宕机了,则其持有的会话会超时,此时该临时节点会被删除,相应的监听事件就会被触发。 ++ 分布式系统的每个服务节点还可以将自己的节点状态写入临时节点,从而完成状态报告或节点工作进度汇报。 ++ 通过数据的订阅和发布功能,Zookeeper 还能对分布式系统进行模块的解耦和任务的调度。 ++ 通过监听机制,还能对分布式系统的服务节点进行动态上下线,从而实现服务的动态扩容。 + +
+ +## 参考资料 + +1. 倪超 . 从 Paxos 到 Zookeeper——分布式一致性原理与实践 . 电子工业出版社 . 2015-02-01 + + + + + + + + + + + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Azkaban_3.x_\347\274\226\350\257\221\345\217\212\351\203\250\347\275\262.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Azkaban_3.x_\347\274\226\350\257\221\345\217\212\351\203\250\347\275\262.md" new file mode 100644 index 0000000..aba02df --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Azkaban_3.x_\347\274\226\350\257\221\345\217\212\351\203\250\347\275\262.md" @@ -0,0 +1,124 @@ +# Azkaban 3.x 编译及部署 + + + + + +## 一、Azkaban 源码编译 + +### 1.1 下载并解压 + +Azkaban 在 3.0 版本之后就不提供对应的安装包,需要自己下载源码进行编译。 + +下载所需版本的源码,Azkaban 的源码托管在 GitHub 上,地址为 https://github.com/azkaban/azkaban 。可以使用 `git clone` 的方式获取源码,也可以使用 `wget` 直接下载对应 release 版本的 `tar.gz` 文件,这里我采用第二种方式: + +```shell +# 下载 +wget https://github.com/azkaban/azkaban/archive/3.70.0.tar.gz +# 解压 +tar -zxvf azkaban-3.70.0.tar.gz +``` + +### 1.2 准备编译环境 + +#### 1. JDK + +Azkaban 编译依赖 JDK 1.8+ ,JDK 安装方式见本仓库: + +> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + +#### 2. Gradle + +Azkaban 3.70.0 编译需要依赖 `gradle-4.6-all.zip`。Gradle 是一个项目自动化构建开源工具,类似于 Maven,但由于采用 Groovy 语言进行项目配置,所以比 Maven 更为灵活,目前广泛用于 Android 开发、Spring 项目的构建。 + +需要注意的是不同版本的 Azkaban 依赖 Gradle 版本不同,可以在解压后的 `/gradle/wrapper/gradle-wrapper.properties` 文件查看 + +
+ +在编译时程序会自动去图中所示的地址进行下载,但是下载速度很慢。为避免影响编译过程,建议先手动下载至 `/gradle/wrapper/` 目录下: + +```shell +# wget https://services.gradle.org/distributions/gradle-4.6-all.zip +``` + +然后修改配置文件 `gradle-wrapper.properties` 中的 `distributionUrl` 属性,指明使用本地的 gradle。 + +
+ +#### 3. Git + +Azkaban 的编译过程需要用 Git 下载部分 JAR 包,所以需要预先安装 Git: + +```shell +# yum install git +``` + +### 1.3 项目编译 + +在根目录下执行编译命令,编译成功后会有 `BUILD SUCCESSFUL` 的提示: + +```shell +# ./gradlew build installDist -x test +``` + +编译过程中需要注意以下问题: + ++ 因为编译的过程需要下载大量的 Jar 包,下载速度根据网络情况而定,通常都不会很快,如果网络不好,耗费半个小时,一个小时都是很正常的; ++ 编译过程中如果出现网络问题而导致 JAR 无法下载,编译可能会被强行终止,这时候重复执行编译命令即可,gradle 会把已经下载的 JAR 缓存到本地,所以不用担心会重复下载 JAR 包。 + + + +## 二、Azkaban 部署模式 + +>After version 3.0, we provide two modes: the stand alone “solo-server” mode and distributed multiple-executor mode. The following describes thedifferences between the two modes. + +按照官方文档的说明,Azkaban 3.x 之后版本提供 2 种运行模式: + ++ **solo server model(单服务模式)** :元数据默认存放在内置的 H2 数据库(可以修改为 MySQL),该模式中 `webServer`(管理服务器) 和 `executorServer`(执行服务器) 运行在同一个进程中,进程名是 `AzkabanSingleServer`。该模式适用于小规模工作流的调度。 +- **multiple-executor(分布式多服务模式)** :存放元数据的数据库为 MySQL,MySQL 应采用主从模式进行备份和容错。这种模式下 `webServer` 和 `executorServer` 在不同进程中运行,彼此之间互不影响,适合用于生产环境。 + +下面主要介绍 `Solo Server` 模式。 + + + +## 三 、Solo Server 模式部署 + +### 2.1 解压 + +Solo Server 模式安装包在编译后的 `/azkaban-solo-server/build/distributions` 目录下,找到后进行解压即可: + +```shell +# 解压 +tar -zxvf azkaban-solo-server-3.70.0.tar.gz +``` + +### 2.2 修改时区 + +这一步不是必须的。但是因为 Azkaban 默认采用的时区是 `America/Los_Angeles`,如果你的调度任务中有定时任务的话,就需要进行相应的更改,这里我更改为常用的 `Asia/Shanghai` + +
+ +### 2.3 启动 + +执行启动命令,需要注意的是一定要在根目录下执行,不能进入 `bin` 目录下执行,不然会抛出 `Cannot find 'database.properties'` 异常。 + +```shell +# bin/start-solo.sh +``` + +### 2.4 验证 + +验证方式一:使用 `jps` 命令查看是否有 `AzkabanSingleServer` 进程: + +
+
+ +验证方式二:访问 8081 端口,查看 Web UI 界面,默认的登录名密码都是 `azkaban`,如果需要修改或新增用户,可以在 `conf/azkaban-users.xml ` 文件中进行配置: + +
+ + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Flink_Standalone_Cluster.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Flink_Standalone_Cluster.md" new file mode 100644 index 0000000..4135aea --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Flink_Standalone_Cluster.md" @@ -0,0 +1,270 @@ +# Flink Standalone Cluster + + + +## 一、部署模式 + +Flink 支持使用多种部署模式来满足不同规模应用的需求,常见的有单机模式,Standalone Cluster 模式,同时 Flink 也支持部署在其他第三方平台上,如 YARN,Mesos,Docker,Kubernetes 等。以下主要介绍其单机模式和 Standalone Cluster 模式的部署。 + +## 二、单机模式 + +单机模式是一种开箱即用的模式,可以在单台服务器上运行,适用于日常的开发和调试。具体操作步骤如下: + +### 2.1 安装部署 + +**1. 前置条件** + +Flink 的运行依赖 JAVA 环境,故需要预先安装好 JDK,具体步骤可以参考:[Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + +**2. 下载 & 解压 & 运行** + +Flink 所有版本的安装包可以直接从其[官网](https://flink.apache.org/downloads.html)进行下载,这里我下载的 Flink 的版本为 `1.9.1` ,要求的 JDK 版本为 `1.8.x +`。 下载后解压到指定目录: + +```shell +tar -zxvf flink-1.9.1-bin-scala_2.12.tgz -C /usr/app +``` + +不需要进行任何配置,直接使用以下命令就可以启动单机版本的 Flink: + +```shell +bin/start-cluster.sh +``` + +**3. WEB UI 界面** + +Flink 提供了 WEB 界面用于直观的管理 Flink 集群,访问端口为 `8081`: + +
+ + + +Flink 的 WEB UI 界面支持大多数常用功能,如提交作业,取消作业,查看各个节点运行情况,查看作业执行情况等,大家可以在部署完成后,进入该页面进行详细的浏览。 + +### 2.2 作业提交 + +启动后可以运行安装包中自带的词频统计案例,具体步骤如下: + +**1. 开启端口** + +```shell +nc -lk 9999 +``` + +**2. 提交作业** + +```shell +bin/flink run examples/streaming/SocketWindowWordCount.jar --port 9999 +``` + +该 JAR 包的源码可以在 Flink 官方的 GitHub 仓库中找到,地址为 :[SocketWindowWordCount](https://github.com/apache/flink/blob/master/flink-examples/flink-examples-streaming/src/main/java/org/apache/flink/streaming/examples/socket/SocketWindowWordCount.java) ,可选传参有 hostname, port,对应的词频数据需要使用空格进行分割。 + +**3. 输入测试数据** + +```shell +a a b b c c c a e +``` + +**4. 查看控制台输出** + +可以通过 WEB UI 的控制台查看作业统运行情况: + +
+ + + +也可以通过 WEB 控制台查看到统计结果: + +
+ + + +### 2.3 停止作业 + +可以直接在 WEB 界面上点击对应作业的 `Cancel Job` 按钮进行取消,也可以使用命令行进行取消。使用命令行进行取消时,需要先获取到作业的 JobId,可以使用 `flink list` 命令查看,输出如下: + +```shell +[root@hadoop001 flink-1.9.1]# ./bin/flink list +Waiting for response... +------------------ Running/Restarting Jobs ------------------- +05.11.2019 08:19:53 : ba2b1cc41a5e241c32d574c93de8a2bc : Socket Window WordCount (RUNNING) +-------------------------------------------------------------- +No scheduled jobs. +``` + +获取到 JobId 后,就可以使用 `flink cancel` 命令取消作业: + +```shell +bin/flink cancel ba2b1cc41a5e241c32d574c93de8a2bc +``` + +### 2.4 停止 Flink + +命令如下: + +```shell +bin/stop-cluster.sh +``` + + + +## 三、Standalone Cluster + +Standalone Cluster 模式是 Flink 自带的一种集群模式,具体配置步骤如下: + +### 3.1 前置条件 + +使用该模式前,需要确保所有服务器间都已经配置好 SSH 免密登录服务。这里我以三台服务器为例,主机名分别为 hadoop001,hadoop002,hadoop003 , 其中 hadoop001 为 master 节点,其余两台为 slave 节点,搭建步骤如下: + +### 3.2 搭建步骤 + +修改 `conf/flink-conf.yaml` 中 jobmanager 节点的通讯地址为 hadoop001: + +```yaml +jobmanager.rpc.address: hadoop001 +``` + +修改 `conf/slaves` 配置文件,将 hadoop002 和 hadoop003 配置为 slave 节点: + +```shell +hadoop002 +hadoop003 +``` + +将配置好的 Flink 安装包分发到其他两台服务器上: + +```shell + scp -r /usr/app/flink-1.9.1 hadoop002:/usr/app + scp -r /usr/app/flink-1.9.1 hadoop003:/usr/app +``` + +在 hadoop001 上使用和单机模式相同的命令来启动集群: + +```shell +bin/start-cluster.sh +``` + +此时控制台输出如下: + +
+ + + +启动完成后可以使用 `Jps` 命令或者通过 WEB 界面来查看是否启动成功。 + +### 3.3 可选配置 + +除了上面介绍的 *jobmanager.rpc.address* 是必选配置外,Flink h还支持使用其他可选参数来优化集群性能,主要如下: + +- **jobmanager.heap.size**:JobManager 的 JVM 堆内存大小,默认为 1024m 。 +- **taskmanager.heap.size**:Taskmanager 的 JVM 堆内存大小,默认为 1024m 。 +- **taskmanager.numberOfTaskSlots**:Taskmanager 上 slots 的数量,通常设置为 CPU 核心的数量,或其一半。 +- **parallelism.default**:任务默认的并行度。 +- **io.tmp.dirs**:存储临时文件的路径,如果没有配置,则默认采用服务器的临时目录,如 LInux 的 `/tmp` 目录。 + +更多配置可以参考 Flink 的官方手册:[Configuration](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/config.html) + +## 四、Standalone Cluster HA + +上面我们配置的 Standalone 集群实际上只有一个 JobManager,此时是存在单点故障的,所以官方提供了 Standalone Cluster HA 模式来实现集群高可用。 + +### 4.1 前置条件 + +在 Standalone Cluster HA 模式下,集群可以由多个 JobManager,但只有一个处于 active 状态,其余的则处于备用状态,Flink 使用 ZooKeeper 来选举出 Active JobManager,并依赖其来提供一致性协调服务,所以需要预先安装 ZooKeeper 。 + +另外在高可用模式下,还需要使用分布式文件系统来持久化存储 JobManager 的元数据,最常用的就是 HDFS,所以 Hadoop 也需要预先安装。关于 Hadoop 集群和 ZooKeeper 集群的搭建可以参考: + ++ [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md) ++ [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) + +### 4.2 搭建步骤 + +修改 `conf/flink-conf.yaml` 文件,增加如下配置: + +```yaml +# 配置使用zookeeper来开启高可用模式 +high-availability: zookeeper +# 配置zookeeper的地址,采用zookeeper集群时,可以使用逗号来分隔多个节点地址 +high-availability.zookeeper.quorum: hadoop003:2181 +# 在zookeeper上存储flink集群元信息的路径 +high-availability.zookeeper.path.root: /flink +# 集群id +high-availability.cluster-id: /standalone_cluster_one +# 持久化存储JobManager元数据的地址,zookeeper上存储的只是指向该元数据的指针信息 +high-availability.storageDir: hdfs://hadoop001:8020/flink/recovery +``` + +修改 `conf/masters` 文件,将 hadoop001 和 hadoop002 都配置为 master 节点: + +```shell +hadoop001:8081 +hadoop002:8081 +``` + +确保 Hadoop 和 ZooKeeper 已经启动后,使用以下命令来启动集群: + +```shell +bin/start-cluster.sh +``` + +此时输出如下: + +
+ + + +可以看到集群已经以 HA 的模式启动,此时还需要在各个节点上使用 `jps` 命令来查看进程是否启动成功,正常情况如下: + +
+ + + +只有 hadoop001 和 hadoop002 的 JobManager 进程,hadoop002 和 hadoop003 上的 TaskManager 进程都已经完全启动,才表示 Standalone Cluster HA 模式搭建成功。 + +### 4.3 常见异常 + +如果进程没有启动,可以通过查看 `log` 目录下的日志来定位错误,常见的一个错误如下: + +```shell +2019-11-05 09:18:35,877 INFO org.apache.flink.runtime.entrypoint.ClusterEntrypoint +- Shutting StandaloneSessionClusterEntrypoint down with application status FAILED. Diagnostics +java.io.IOException: Could not create FileSystem for highly available storage (high-availability.storageDir) +....... +Caused by: org.apache.flink.core.fs.UnsupportedFileSystemSchemeException: Could not find a file +system implementation for scheme 'hdfs'. The scheme is not directly supported by Flink and no +Hadoop file system to support this scheme could be loaded. +..... +Caused by: org.apache.flink.core.fs.UnsupportedFileSystemSchemeException: Hadoop is not in +the classpath/dependencies. +...... +``` + +可以看到是因为在 classpath 目录下找不到 Hadoop 的相关依赖,此时需要检查是否在环境变量中配置了 Hadoop 的安装路径,如果路径已经配置但仍然存在上面的问题,可以从 [Flink 官网](https://flink.apache.org/downloads.html)下载对应版本的 Hadoop 组件包: + +
+ + + +下载完成后,将该 JAR 包上传至**所有** Flink 安装目录的 `lib` 目录即可。 + + + +## 参考资料 + ++ [Standalone Cluster](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/deployment/cluster_setup.html#standalone-cluster) ++ [JobManager High Availability (HA)](https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/jobmanager_high_availability.html) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..f41a44e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,227 @@ +# HBase基本环境搭建 + + + +## 一、安装前置条件说明 + +### 1.1 JDK版本说明 + +HBase 需要依赖 JDK 环境,同时 HBase 2.0+ 以上版本不再支持 JDK 1.7 ,需要安装 JDK 1.8+ 。JDK 安装方式见本仓库: + +> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + +### 1.2 Standalone模式和伪集群模式的区别 + ++ 在 `Standalone` 模式下,所有守护进程都运行在一个 `jvm` 进程/实例中; ++ 在伪分布模式下,HBase 仍然在单个主机上运行,但是每个守护进程 (HMaster,HRegionServer 和 ZooKeeper) 则分别作为一个单独的进程运行。 + +**说明:两种模式任选其一进行部署即可,对于开发测试来说区别不大。** + + + +## 二、Standalone 模式 + +### 2.1 下载并解压 + +从[官方网站](https://hbase.apache.org/downloads.html) 下载所需要版本的二进制安装包,并进行解压: + +```shell +# tar -zxvf hbase-2.1.4-bin.tar.gz +``` + +### 2.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export HBASE_HOME=/usr/app/hbase-2.1.4 +export PATH=$HBASE_HOME/bin:$PATH +``` + +使得配置的环境变量生效: + +```shell +# source /etc/profile +``` + +### 2.3 进行HBase相关配置 + +修改安装目录下的 `conf/hbase-env.sh`,指定 JDK 的安装路径: + +```shell +# The java implementation to use. Java 1.8+ required. +export JAVA_HOME=/usr/java/jdk1.8.0_201 +``` + +修改安装目录下的 `conf/hbase-site.xml`,增加如下配置: + +```xml + + + hbase.rootdir + file:///home/hbase/rootdir + + + hbase.zookeeper.property.dataDir + /home/zookeeper/dataDir + + + hbase.unsafe.stream.capability.enforce + false + + +``` + +`hbase.rootdir`: 配置 hbase 数据的存储路径; + +`hbase.zookeeper.property.dataDir`: 配置 zookeeper 数据的存储路径; + +`hbase.unsafe.stream.capability.enforce`: 使用本地文件系统存储,不使用 HDFS 的情况下需要禁用此配置,设置为 false。 + +### 2.4 启动HBase + +由于已经将 HBase 的 bin 目录配置到环境变量,直接使用以下命令启动: + +```shell +# start-hbase.sh +``` + +### 2.5 验证启动是否成功 + +验证方式一 :使用 `jps` 命令查看 HMaster 进程是否启动。 + +``` +[root@hadoop001 hbase-2.1.4]# jps +16336 Jps +15500 HMaster +``` + +验证方式二 :访问 HBaseWeb UI 页面,默认端口为 `16010` 。 + +
+ + +## 三、伪集群模式安装(Pseudo-Distributed) + +### 3.1 Hadoop单机伪集群安装 + +这里我们采用 HDFS 作为 HBase 的存储方案,需要预先安装 Hadoop。Hadoop 的安装方式单独整理至: + +> [Hadoop 单机伪集群搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop单机版本环境搭建.md) + +### 3.2 Hbase版本选择 + +HBase 的版本必须要与 Hadoop 的版本兼容,不然会出现各种 Jar 包冲突。这里我 Hadoop 安装的版本为 `hadoop-2.6.0-cdh5.15.2`,为保持版本一致,选择的 HBase 版本为 `hbase-1.2.0-cdh5.15.2` 。所有软件版本如下: + ++ Hadoop 版本: hadoop-2.6.0-cdh5.15.2 ++ HBase 版本: hbase-1.2.0-cdh5.15.2 ++ JDK 版本:JDK 1.8 + + + +### 3.3 软件下载解压 + +下载后进行解压,下载地址:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# tar -zxvf hbase-1.2.0-cdh5.15.2.tar.gz +``` + +### 3.4 配置环境变量 +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export HBASE_HOME=/usr/app/hbase-1.2.0-cdh5.15.2 +export PATH=$HBASE_HOME/bin:$PATH +``` + +使得配置的环境变量生效: + +```shell +# source /etc/profile +``` + + + + +### 3.5 进行HBase相关配置 + +1.修改安装目录下的 `conf/hbase-env.sh`,指定 JDK 的安装路径: + +```shell +# The java implementation to use. Java 1.7+ required. +export JAVA_HOME=/usr/java/jdk1.8.0_201 +``` + +2.修改安装目录下的 `conf/hbase-site.xml`,增加如下配置 (hadoop001 为主机名): + +```xml + + + + hbase.cluster.distributed + true + + + + hbase.rootdir + hdfs://hadoop001:8020/hbase + + + + hbase.zookeeper.property.dataDir + /home/zookeeper/dataDir + + +``` + +3.修改安装目录下的 `conf/regionservers`,指定 region servers 的地址,修改后其内容如下: + +```shell +hadoop001 +``` + + + +### 3.6 启动 + +```shell +# bin/start-hbase.sh +``` + + + +### 3.7 验证启动是否成功 + +验证方式一 :使用 `jps` 命令查看进程。其中 `HMaster`,`HRegionServer` 是 HBase 的进程,`HQuorumPeer` 是 HBase 内置的 Zookeeper 的进程,其余的为 HDFS 和 YARN 的进程。 + +```shell +[root@hadoop001 conf]# jps +28688 NodeManager +25824 GradleDaemon +10177 Jps +22083 HRegionServer +20534 DataNode +20807 SecondaryNameNode +18744 Main +20411 NameNode +21851 HQuorumPeer +28573 ResourceManager +21933 HMaster +``` + +验证方式二 :访问 HBase Web UI 界面,需要注意的是 1.2 版本的 HBase 的访问端口为 `60010` + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..6e3cf6d --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/HBase\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,200 @@ +# HBase集群环境配置 + + + + + +## 一、集群规划 + +这里搭建一个 3 节点的 HBase 集群,其中三台主机上均为 `Regin Server`。同时为了保证高可用,除了在 hadoop001 上部署主 `Master` 服务外,还在 hadoop002 上部署备用的 `Master` 服务。Master 服务由 Zookeeper 集群进行协调管理,如果主 `Master` 不可用,则备用 `Master` 会成为新的主 `Master`。 + +
+ +## 二、前置条件 + +HBase 的运行需要依赖 Hadoop 和 JDK(`HBase 2.0+` 对应 `JDK 1.8+`) 。同时为了保证高可用,这里我们不采用 HBase 内置的 Zookeeper 服务,而采用外置的 Zookeeper 集群。相关搭建步骤可以参阅: + +- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) +- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) +- [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md) + + + +## 三、集群搭建 + +### 3.1 下载并解压 + +下载并解压,这里我下载的是 CDH 版本 HBase,下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# tar -zxvf hbase-1.2.0-cdh5.15.2.tar.gz +``` + +### 3.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export HBASE_HOME=usr/app/hbase-1.2.0-cdh5.15.2 +export PATH=$HBASE_HOME/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 3.3 集群配置 + +进入 `${HBASE_HOME}/conf` 目录下,修改配置: + +#### 1. hbase-env.sh + +```shell +# 配置JDK安装位置 +export JAVA_HOME=/usr/java/jdk1.8.0_201 +# 不使用内置的zookeeper服务 +export HBASE_MANAGES_ZK=false +``` + +#### 2. hbase-site.xml + +```xml + + + + hbase.cluster.distributed + true + + + + hbase.rootdir + hdfs://hadoop001:8020/hbase + + + + hbase.zookeeper.quorum + hadoop001:2181,hadoop002:2181,hadoop003:2181 + + +``` + +#### 3. regionservers + +``` +hadoop001 +hadoop002 +hadoop003 +``` + +#### 4. backup-masters + +``` +hadoop002 +``` + +` backup-masters` 这个文件是不存在的,需要新建,主要用来指明备用的 master 节点,可以是多个,这里我们以 1 个为例。 + +### 3.4 HDFS客户端配置 + +这里有一个可选的配置:如果您在 Hadoop 集群上进行了 HDFS 客户端配置的更改,比如将副本系数 `dfs.replication` 设置成 5,则必须使用以下方法之一来使 HBase 知道,否则 HBase 将依旧使用默认的副本系数 3 来创建文件: + +> 1. Add a pointer to your `HADOOP_CONF_DIR` to the `HBASE_CLASSPATH` environment variable in *hbase-env.sh*. +> 2. Add a copy of *hdfs-site.xml* (or *hadoop-site.xml*) or, better, symlinks, under *${HBASE_HOME}/conf*, or +> 3. if only a small set of HDFS client configurations, add them to *hbase-site.xml*. + +以上是官方文档的说明,这里解释一下: + +**第一种** :将 Hadoop 配置文件的位置信息添加到 `hbase-env.sh` 的 `HBASE_CLASSPATH` 属性,示例如下: + +```shell +export HBASE_CLASSPATH=usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop +``` + +**第二种** :将 Hadoop 的 ` hdfs-site.xml` 或 `hadoop-site.xml` 拷贝到 `${HBASE_HOME}/conf ` 目录下,或者通过符号链接的方式。如果采用这种方式的话,建议将两者都拷贝或建立符号链接,示例如下: + +```shell +# 拷贝 +cp core-site.xml hdfs-site.xml /usr/app/hbase-1.2.0-cdh5.15.2/conf/ +# 使用符号链接 +ln -s /usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop/core-site.xml +ln -s /usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop/hdfs-site.xml +``` + +> 注:`hadoop-site.xml` 这个配置文件现在叫做 `core-site.xml` + +**第三种** :如果你只有少量更改,那么直接配置到 `hbase-site.xml` 中即可。 + + + +### 3.5 安装包分发 + +将 HBase 的安装包分发到其他服务器,分发后建议在这两台服务器上也配置一下 HBase 的环境变量。 + +```shell +scp -r /usr/app/hbase-1.2.0-cdh5.15.2/ hadoop002:usr/app/ +scp -r /usr/app/hbase-1.2.0-cdh5.15.2/ hadoop003:usr/app/ +``` + + + +## 四、启动集群 + +### 4.1 启动ZooKeeper集群 + +分别到三台服务器上启动 ZooKeeper 服务: + +```shell + zkServer.sh start +``` + +### 4.2 启动Hadoop集群 + +```shell +# 启动dfs服务 +start-dfs.sh +# 启动yarn服务 +start-yarn.sh +``` + +### 4.3 启动HBase集群 + +进入 hadoop001 的 `${HBASE_HOME}/bin`,使用以下命令启动 HBase 集群。执行此命令后,会在 hadoop001 上启动 `Master` 服务,在 hadoop002 上启动备用 `Master` 服务,在 `regionservers` 文件中配置的所有节点启动 `region server` 服务。 + +```shell +start-hbase.sh +``` + + + +### 4.5 查看服务 + +访问 HBase 的 Web-UI 界面,这里我安装的 HBase 版本为 1.2,访问端口为 `60010`,如果你安装的是 2.0 以上的版本,则访问端口号为 `16010`。可以看到 `Master` 在 hadoop001 上,三个 `Regin Servers` 分别在 hadoop001,hadoop002,和 hadoop003 上,并且还有一个 `Backup Matser` 服务在 hadoop002 上。 + +
+
+ +hadoop002 上的 HBase 出于备用状态: + +
+ +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..d9ef973 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,262 @@ +# Hadoop单机版环境搭建 + + + + + + +## 一、前置条件 + +Hadoop 的运行依赖 JDK,需要预先安装,安装步骤见: + ++ [Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + + + +## 二、配置免密登录 + +Hadoop 组件之间需要基于 SSH 进行通讯。 + +#### 2.1 配置映射 + +配置 ip 地址和主机名映射: + +```shell +vim /etc/hosts +# 文件末尾增加 +192.168.43.202 hadoop001 +``` + +### 2.2 生成公私钥 + +执行下面命令行生成公匙和私匙: + +``` +ssh-keygen -t rsa +``` + +### 3.3 授权 + +进入 `~/.ssh` 目录下,查看生成的公匙和私匙,并将公匙写入到授权文件: + +```shell +[root@@hadoop001 sbin]# cd ~/.ssh +[root@@hadoop001 .ssh]# ll +-rw-------. 1 root root 1675 3 月 15 09:48 id_rsa +-rw-r--r--. 1 root root 388 3 月 15 09:48 id_rsa.pub +``` + +```shell +# 写入公匙到授权文件 +[root@hadoop001 .ssh]# cat id_rsa.pub >> authorized_keys +[root@hadoop001 .ssh]# chmod 600 authorized_keys +``` + + + +## 三、Hadoop(HDFS)环境搭建 + + + +### 3.1 下载并解压 + +下载 Hadoop 安装包,这里我下载的是 CDH 版本的,下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# 解压 +tar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz +``` + + + +### 3.2 配置环境变量 + +```shell +# vi /etc/profile +``` + +配置环境变量: + +``` +export HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 +export PATH=${HADOOP_HOME}/bin:$PATH +``` + +执行 `source` 命令,使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + + + +### 3.3 修改Hadoop配置 + +进入 `${HADOOP_HOME}/etc/hadoop/ ` 目录下,修改以下配置: + +#### 1. hadoop-env.sh + +```shell +# JDK安装路径 +export JAVA_HOME=/usr/java/jdk1.8.0_201/ +``` + +#### 2. core-site.xml + +```xml + + + + fs.defaultFS + hdfs://hadoop001:8020 + + + + hadoop.tmp.dir + /home/hadoop/tmp + + +``` + +#### 3. hdfs-site.xml + +指定副本系数和临时文件存储位置: + +```xml + + + + dfs.replication + 1 + + +``` + +#### 4. slaves + +配置所有从属节点的主机名或 IP 地址,由于是单机版本,所以指定本机即可: + +```shell +hadoop001 +``` + + + +### 3.4 关闭防火墙 + +不关闭防火墙可能导致无法访问 Hadoop 的 Web UI 界面: + +```shell +# 查看防火墙状态 +sudo firewall-cmd --state +# 关闭防火墙: +sudo systemctl stop firewalld.service +``` + + + +### 3.5 初始化 + +第一次启动 Hadoop 时需要进行初始化,进入 `${HADOOP_HOME}/bin/` 目录下,执行以下命令: + +```shell +[root@hadoop001 bin]# ./hdfs namenode -format +``` + + + +### 3.6 启动HDFS + +进入 `${HADOOP_HOME}/sbin/` 目录下,启动 HDFS: + +```shell +[root@hadoop001 sbin]# ./start-dfs.sh +``` + + + +### 3.7 验证是否启动成功 + +方式一:执行 `jps` 查看 `NameNode` 和 `DataNode` 服务是否已经启动: + +```shell +[root@hadoop001 hadoop-2.6.0-cdh5.15.2]# jps +9137 DataNode +9026 NameNode +9390 SecondaryNameNode +``` + + + +方式二:查看 Web UI 界面,端口为 `50070`: + +
+ + +## 四、Hadoop(YARN)环境搭建 + +### 4.1 修改配置 + +进入 `${HADOOP_HOME}/etc/hadoop/ ` 目录下,修改以下配置: + +#### 1. mapred-site.xml + +```shell +# 如果没有mapred-site.xml,则拷贝一份样例文件后再修改 +cp mapred-site.xml.template mapred-site.xml +``` + +```xml + + + mapreduce.framework.name + yarn + + +``` + +#### 2. yarn-site.xml + +```xml + + + + yarn.nodemanager.aux-services + mapreduce_shuffle + + +``` + + + +### 4.2 启动服务 + +进入 `${HADOOP_HOME}/sbin/` 目录下,启动 YARN: + +```shell +./start-yarn.sh +``` + + + +#### 4.3 验证是否启动成功 + +方式一:执行 `jps` 命令查看 `NodeManager` 和 `ResourceManager` 服务是否已经启动: + +```shell +[root@hadoop001 hadoop-2.6.0-cdh5.15.2]# jps +9137 DataNode +9026 NameNode +12294 NodeManager +12185 ResourceManager +9390 SecondaryNameNode +``` + +方式二:查看 Web UI 界面,端口号为 `8088`: + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..64e4009 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Hadoop\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,233 @@ +# Hadoop集群环境搭建 + + + + +## 一、集群规划 + +这里搭建一个 3 节点的 Hadoop 集群,其中三台主机均部署 `DataNode` 和 `NodeManager` 服务,但只有 hadoop001 上部署 `NameNode` 和 `ResourceManager` 服务。 + +
+ +## 二、前置条件 + +Hadoop 的运行依赖 JDK,需要预先安装。其安装步骤单独整理至: + ++ [Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + + + +## 三、配置免密登录 + +### 3.1 生成密匙 + +在每台主机上使用 `ssh-keygen` 命令生成公钥私钥对: + +```shell +ssh-keygen +``` + +### 3.2 免密登录 + +将 `hadoop001` 的公钥写到本机和远程机器的 ` ~/ .ssh/authorized_key` 文件中: + +```shell +ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop001 +ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop002 +ssh-copy-id -i ~/.ssh/id_rsa.pub hadoop003 +``` + +### 3.3 验证免密登录 + +```she +ssh hadoop002 +ssh hadoop003 +``` + + + +## 四、集群搭建 + +### 3.1 下载并解压 + +下载 Hadoop。这里我下载的是 CDH 版本 Hadoop,下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# tar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz +``` + +### 3.2 配置环境变量 + +编辑 `profile` 文件: + +```shell +# vim /etc/profile +``` + +增加如下配置: + +``` +export HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 +export PATH=${HADOOP_HOME}/bin:$PATH +``` + +执行 `source` 命令,使得配置立即生效: + +```shell +# source /etc/profile +``` + +### 3.3 修改配置 + +进入 `${HADOOP_HOME}/etc/hadoop` 目录下,修改配置文件。各个配置文件内容如下: + +#### 1. hadoop-env.sh + +```shell +# 指定JDK的安装位置 +export JAVA_HOME=/usr/java/jdk1.8.0_201/ +``` + +#### 2. core-site.xml + +```xml + + + + fs.defaultFS + hdfs://hadoop001:8020 + + + + hadoop.tmp.dir + /home/hadoop/tmp + + +``` + +#### 3. hdfs-site.xml + +```xml + + + dfs.namenode.name.dir + /home/hadoop/namenode/data + + + + dfs.datanode.data.dir + /home/hadoop/datanode/data + +``` + +#### 4. yarn-site.xml + +```xml + + + + yarn.nodemanager.aux-services + mapreduce_shuffle + + + + yarn.resourcemanager.hostname + hadoop001 + + + +``` + +#### 5. mapred-site.xml + +```xml + + + + mapreduce.framework.name + yarn + + +``` + +#### 5. slaves + +配置所有从属节点的主机名或 IP 地址,每行一个。所有从属节点上的 `DataNode` 服务和 `NodeManager` 服务都会被启动。 + +```properties +hadoop001 +hadoop002 +hadoop003 +``` + +### 3.4 分发程序 + +将 Hadoop 安装包分发到其他两台服务器,分发后建议在这两台服务器上也配置一下 Hadoop 的环境变量。 + +```shell +# 将安装包分发到hadoop002 +scp -r /usr/app/hadoop-2.6.0-cdh5.15.2/ hadoop002:/usr/app/ +# 将安装包分发到hadoop003 +scp -r /usr/app/hadoop-2.6.0-cdh5.15.2/ hadoop003:/usr/app/ +``` + +### 3.5 初始化 + +在 `Hadoop001` 上执行 namenode 初始化命令: + +``` +hdfs namenode -format +``` + +### 3.6 启动集群 + +进入到 `Hadoop001` 的 `${HADOOP_HOME}/sbin` 目录下,启动 Hadoop。此时 `hadoop002` 和 `hadoop003` 上的相关服务也会被启动: + +```shell +# 启动dfs服务 +start-dfs.sh +# 启动yarn服务 +start-yarn.sh +``` + +### 3.7 查看集群 + +在每台服务器上使用 `jps` 命令查看服务进程,或直接进入 Web-UI 界面进行查看,端口为 `50070`。可以看到此时有三个可用的 `Datanode`: + +
+
+ +点击 `Live Nodes` 进入,可以看到每个 `DataNode` 的详细情况: + +
+
+ +接着可以查看 Yarn 的情况,端口号为 `8088` : + +
+ + +## 五、提交服务到集群 + +提交作业到集群的方式和单机环境完全一致,这里以提交 Hadoop 内置的计算 Pi 的示例程序为例,在任何一个节点上执行都可以,命令如下: + +```shell +hadoop jar /usr/app/hadoop-2.6.0-cdh5.15.2/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Flume\347\232\204\345\256\211\350\243\205.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Flume\347\232\204\345\256\211\350\243\205.md" new file mode 100644 index 0000000..2e8b46a --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Flume\347\232\204\345\256\211\350\243\205.md" @@ -0,0 +1,68 @@ +# Linux下Flume的安装 + + +## 一、前置条件 + +Flume 需要依赖 JDK 1.8+,JDK 安装方式见本仓库: + +> [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + + + +## 二 、安装步骤 + +### 2.1 下载并解压 + +下载所需版本的 Flume,这里我下载的是 `CDH` 版本的 Flume。下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# 下载后进行解压 +tar -zxvf flume-ng-1.6.0-cdh5.15.2.tar.gz +``` + +### 2.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export FLUME_HOME=/usr/app/apache-flume-1.6.0-cdh5.15.2-bin +export PATH=$FLUME_HOME/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 2.3 修改配置 + +进入安装目录下的 `conf/` 目录,拷贝 Flume 的环境配置模板 `flume-env.sh.template`: + +```shell +# cp flume-env.sh.template flume-env.sh +``` + +修改 `flume-env.sh`,指定 JDK 的安装路径: + +```shell +# Enviroment variables can be set here. +export JAVA_HOME=/usr/java/jdk1.8.0_201 +``` + +### 2.4 验证 + +由于已经将 Flume 的 bin 目录配置到环境变量,直接使用以下命令验证是否配置成功: + +```shell +# flume-ng version +``` + +出现对应的版本信息则代表配置成功。 + +![flume-version](https://github.com/heibaiying/BigData-Notes/blob/master/pictures/flume-version.png) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213JDK\345\256\211\350\243\205.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213JDK\345\256\211\350\243\205.md" new file mode 100644 index 0000000..5459e04 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213JDK\345\256\211\350\243\205.md" @@ -0,0 +1,55 @@ +# Linux下JDK的安装 + +>**系统环境**:centos 7.6 +> +>**JDK 版本**:jdk 1.8.0_20 + + + +### 1. 下载并解压 + +在[官网](https://www.oracle.com/technetwork/java/javase/downloads/index.html) 下载所需版本的 JDK,这里我下载的版本为[JDK 1.8](https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) ,下载后进行解压: + +```shell +[root@ java]# tar -zxvf jdk-8u201-linux-x64.tar.gz +``` + + + +### 2. 设置环境变量 + +```shell +[root@ java]# vi /etc/profile +``` + +添加如下配置: + +```shell +export JAVA_HOME=/usr/java/jdk1.8.0_201 +export JRE_HOME=${JAVA_HOME}/jre +export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib +export PATH=${JAVA_HOME}/bin:$PATH +``` + +执行 `source` 命令,使得配置立即生效: + +```shell +[root@ java]# source /etc/profile +``` + + + +### 3. 检查是否安装成功 + +```shell +[root@ java]# java -version +``` + +显示出对应的版本信息则代表安装成功。 + +```shell +java version "1.8.0_201" +Java(TM) SE Runtime Environment (build 1.8.0_201-b09) +Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode) + +``` diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Python\345\256\211\350\243\205.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Python\345\256\211\350\243\205.md" new file mode 100644 index 0000000..20fa191 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\344\270\213Python\345\256\211\350\243\205.md" @@ -0,0 +1,71 @@ +## Linux下Python安装 + +>**系统环境**:centos 7.6 +> +>**Python 版本**:Python-3.6.8 + +### 1. 环境依赖 + +Python3.x 的安装需要依赖这四个组件:gcc, zlib,zlib-devel,openssl-devel;所以需要预先安装,命令如下: + +```shell +yum install gcc -y +yum install zlib -y +yum install zlib-devel -y +yum install openssl-devel -y +``` + +### 2. 下载编译 + +Python 源码包下载地址: https://www.python.org/downloads/ + +```shell +# wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz +``` + +### 3. 解压编译 + +```shell +# tar -zxvf Python-3.6.8.tgz +``` + +进入根目录进行编译,可以指定编译安装的路径,这里我们指定为 `/usr/app/python3.6` : + +```shell +# cd Python-3.6.8 +# ./configure --prefix=/usr/app/python3.6 +# make && make install +``` + +### 4. 环境变量配置 + +```shell +vim /etc/profile +``` + +```shell +export PYTHON_HOME=/usr/app/python3.6 +export PATH=${PYTHON_HOME}/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +source /etc/profile +``` + +### 5. 验证安装是否成功 + +输入 `python3` 命令,如果能进入 python 交互环境,则代表安装成功: + +```shell +[root@hadoop001 app]# python3 +Python 3.6.8 (default, Mar 29 2019, 10:17:41) +[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> 1+1 +2 +>>> exit() +[root@hadoop001 app]# +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\347\216\257\345\242\203\344\270\213Hive\347\232\204\345\256\211\350\243\205\351\203\250\347\275\262.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\347\216\257\345\242\203\344\270\213Hive\347\232\204\345\256\211\350\243\205\351\203\250\347\275\262.md" new file mode 100644 index 0000000..5b02606 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Linux\347\216\257\345\242\203\344\270\213Hive\347\232\204\345\256\211\350\243\205\351\203\250\347\275\262.md" @@ -0,0 +1,181 @@ +# Linux环境下Hive的安装 + + + +## 一、安装Hive + +### 1.1 下载并解压 + +下载所需版本的 Hive,这里我下载版本为 `cdh5.15.2`。下载地址:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# 下载后进行解压 + tar -zxvf hive-1.1.0-cdh5.15.2.tar.gz +``` + +### 1.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export HIVE_HOME=/usr/app/hive-1.1.0-cdh5.15.2 +export PATH=$HIVE_HOME/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 1.3 修改配置 + +**1. hive-env.sh** + +进入安装目录下的 `conf/` 目录,拷贝 Hive 的环境配置模板 `flume-env.sh.template` + +```shell +cp hive-env.sh.template hive-env.sh +``` + +修改 `hive-env.sh`,指定 Hadoop 的安装路径: + +```shell +HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 +``` + +**2. hive-site.xml** + +新建 hive-site.xml 文件,内容如下,主要是配置存放元数据的 MySQL 的地址、驱动、用户名和密码等信息: + +```xml + + + + + + javax.jdo.option.ConnectionURL + jdbc:mysql://hadoop001:3306/hadoop_hive?createDatabaseIfNotExist=true + + + + javax.jdo.option.ConnectionDriverName + com.mysql.jdbc.Driver + + + + javax.jdo.option.ConnectionUserName + root + + + + javax.jdo.option.ConnectionPassword + root + + + +``` + + + +### 1.4 拷贝数据库驱动 + +将 MySQL 驱动包拷贝到 Hive 安装目录的 `lib` 目录下, MySQL 驱动的下载地址为:https://dev.mysql.com/downloads/connector/j/ , 在本仓库的[resources](https://github.com/heibaiying/BigData-Notes/tree/master/resources) 目录下我也上传了一份,有需要的可以自行下载。 + +
+ + + +### 1.5 初始化元数据库 + ++ 当使用的 hive 是 1.x 版本时,可以不进行初始化操作,Hive 会在第一次启动的时候会自动进行初始化,但不会生成所有的元数据信息表,只会初始化必要的一部分,在之后的使用中用到其余表时会自动创建; + ++ 当使用的 hive 是 2.x 版本时,必须手动初始化元数据库。初始化命令: + + ```shell + # schematool 命令在安装目录的 bin 目录下,由于上面已经配置过环境变量,在任意位置执行即可 + schematool -dbType mysql -initSchema + ``` + +这里我使用的是 CDH 的 `hive-1.1.0-cdh5.15.2.tar.gz`,对应 `Hive 1.1.0` 版本,可以跳过这一步。 + +### 1.6 启动 + +由于已经将 Hive 的 bin 目录配置到环境变量,直接使用以下命令启动,成功进入交互式命令行后执行 `show databases` 命令,无异常则代表搭建成功。 + +```shell +# hive +``` + +
+ +在 Mysql 中也能看到 Hive 创建的库和存放元数据信息的表 + +
+ + + +## 二、HiveServer2/beeline + +Hive 内置了 HiveServer 和 HiveServer2 服务,两者都允许客户端使用多种编程语言进行连接,但是 HiveServer 不能处理多个客户端的并发请求,因此产生了 HiveServer2。HiveServer2(HS2)允许远程客户端可以使用各种编程语言向 Hive 提交请求并检索结果,支持多客户端并发访问和身份验证。HS2 是由多个服务组成的单个进程,其包括基于 Thrift 的 Hive 服务(TCP 或 HTTP)和用于 Web UI 的 Jetty Web 服务。 + + HiveServer2 拥有自己的 CLI 工具——Beeline。Beeline 是一个基于 SQLLine 的 JDBC 客户端。由于目前 HiveServer2 是 Hive 开发维护的重点,所以官方更加推荐使用 Beeline 而不是 Hive CLI。以下主要讲解 Beeline 的配置方式。 + + + +### 2.1 修改Hadoop配置 + +修改 hadoop 集群的 core-site.xml 配置文件,增加如下配置,指定 hadoop 的 root 用户可以代理本机上所有的用户。 + +```xml + + hadoop.proxyuser.root.hosts + * + + + hadoop.proxyuser.root.groups + * + +``` + +之所以要配置这一步,是因为 hadoop 2.0 以后引入了安全伪装机制,使得 hadoop 不允许上层系统(如 hive)直接将实际用户传递到 hadoop 层,而应该将实际用户传递给一个超级代理,由该代理在 hadoop 上执行操作,以避免任意客户端随意操作 hadoop。如果不配置这一步,在之后的连接中可能会抛出 `AuthorizationException` 异常。 + +>关于 Hadoop 的用户代理机制,可以参考:[hadoop 的用户代理机制](https://blog.csdn.net/u012948976/article/details/49904675#官方文档解读) 或 [Superusers Acting On Behalf Of Other Users](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/Superusers.html) + + + +### 2.2 启动hiveserver2 + +由于上面已经配置过环境变量,这里直接启动即可: + +```shell +# nohup hiveserver2 & +``` + + + +### 2.3 使用beeline + +可以使用以下命令进入 beeline 交互式命令行,出现 `Connected` 则代表连接成功。 + +```shell +# beeline -u jdbc:hive2://hadoop001:10000 -n root +``` + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..0e586a5 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\345\274\200\345\217\221\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,178 @@ +# Spark开发环境搭建 + + + +## 一、安装Spark + +### 1.1 下载并解压 + +官方下载地址:http://spark.apache.org/downloads.html ,选择 Spark 版本和对应的 Hadoop 版本后再下载: + +
+ +解压安装包: + +```shell +# tar -zxvf spark-2.2.3-bin-hadoop2.6.tgz +``` + + + +### 1.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export SPARK_HOME=/usr/app/spark-2.2.3-bin-hadoop2.6 +export PATH=${SPARK_HOME}/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 1.3 Local模式 + +Local 模式是最简单的一种运行方式,它采用单节点多线程方式运行,不用部署,开箱即用,适合日常测试开发。 + +```shell +# 启动spark-shell +spark-shell --master local[2] +``` + +- **local**:只启动一个工作线程; +- **local[k]**:启动 k 个工作线程; +- **local[*]**:启动跟 cpu 数目相同的工作线程数。 + +
+ +
+ +进入 spark-shell 后,程序已经自动创建好了上下文 `SparkContext`,等效于执行了下面的 Scala 代码: + +```scala +val conf = new SparkConf().setAppName("Spark shell").setMaster("local[2]") +val sc = new SparkContext(conf) +``` + + +## 二、词频统计案例 + +安装完成后可以先做一个简单的词频统计例子,感受 spark 的魅力。准备一个词频统计的文件样本 `wc.txt`,内容如下: + +```txt +hadoop,spark,hadoop +spark,flink,flink,spark +hadoop,hadoop +``` + +在 scala 交互式命令行中执行如下 Scala 语句: + +```scala +val file = spark.sparkContext.textFile("file:///usr/app/wc.txt") +val wordCounts = file.flatMap(line => line.split(",")).map((word => (word, 1))).reduceByKey(_ + _) +wordCounts.collect +``` + +执行过程如下,可以看到已经输出了词频统计的结果: + +
+ +同时还可以通过 Web UI 查看作业的执行情况,访问端口为 `4040`: + +
+ + + + + +## 三、Scala开发环境配置 + +Spark 是基于 Scala 语言进行开发的,分别提供了基于 Scala、Java、Python 语言的 API,如果你想使用 Scala 语言进行开发,则需要搭建 Scala 语言的开发环境。 + +### 3.1 前置条件 + +Scala 的运行依赖于 JDK,所以需要你本机有安装对应版本的 JDK,最新的 Scala 2.12.x 需要 JDK 1.8+。 + +### 3.2 安装Scala插件 + +IDEA 默认不支持 Scala 语言的开发,需要通过插件进行扩展。打开 IDEA,依次点击 **File** => **settings**=> **plugins** 选项卡,搜索 Scala 插件 (如下图)。找到插件后进行安装,并重启 IDEA 使得安装生效。 + +
+ + + +### 3.3 创建Scala项目 + +在 IDEA 中依次点击 **File** => **New** => **Project** 选项卡,然后选择创建 `Scala—IDEA` 工程: + +
+ + + +### 3.4 下载Scala SDK + +#### 1. 方式一 + +此时看到 `Scala SDK` 为空,依次点击 `Create` => `Download` ,选择所需的版本后,点击 `OK` 按钮进行下载,下载完成点击 `Finish` 进入工程。 + +
+ + + +#### 2. 方式二 + +方式一是 Scala 官方安装指南里使用的方式,但下载速度通常比较慢,且这种安装下并没有直接提供 Scala 命令行工具。所以个人推荐到官网下载安装包进行安装,下载地址:https://www.scala-lang.org/download/ + +这里我的系统是 Windows,下载 msi 版本的安装包后,一直点击下一步进行安装,安装完成后会自动配置好环境变量。 + +
+ + + +由于安装时已经自动配置好环境变量,所以 IDEA 会自动选择对应版本的 SDK。 + +
+ + + +### 3.5 创建Hello World + +在工程 `src` 目录上右击 **New** => **Scala class** 创建 `Hello.scala`。输入代码如下,完成后点击运行按钮,成功运行则代表搭建成功。 + +
+ + + + + +### 3.6 切换Scala版本 + +在日常的开发中,由于对应软件(如 Spark)的版本切换,可能导致需要切换 Scala 的版本,则可以在 `Project Structures` 中的 `Global Libraries` 选项卡中进行切换。 + +
+ + + + + +### 3.7 可能出现的问题 + +在 IDEA 中有时候重新打开项目后,右击并不会出现新建 `scala` 文件的选项,或者在编写时没有 Scala 语法提示,此时可以先删除 `Global Libraries` 中配置好的 SDK,之后再重新添加: + +
+ + + +**另外在 IDEA 中以本地模式运行 Spark 项目是不需要在本机搭建 Spark 和 Hadoop 环境的。** + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..e0a2d45 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Spark\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,190 @@ +# 基于ZooKeeper搭建Spark高可用集群 + + + + +## 一、集群规划 + +这里搭建一个 3 节点的 Spark 集群,其中三台主机上均部署 `Worker` 服务。同时为了保证高可用,除了在 hadoop001 上部署主 `Master` 服务外,还在 hadoop002 和 hadoop003 上分别部署备用的 `Master` 服务,Master 服务由 Zookeeper 集群进行协调管理,如果主 `Master` 不可用,则备用 `Master` 会成为新的主 `Master`。 + +
+ +## 二、前置条件 + +搭建 Spark 集群前,需要保证 JDK 环境、Zookeeper 集群和 Hadoop 集群已经搭建,相关步骤可以参阅: + +- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) +- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) +- [Hadoop 集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Hadoop集群环境搭建.md) + +## 三、Spark集群搭建 + +### 3.1 下载解压 + +下载所需版本的 Spark,官网下载地址:http://spark.apache.org/downloads.html + +
+ + + +下载后进行解压: + +```shell +# tar -zxvf spark-2.2.3-bin-hadoop2.6.tgz +``` + + + +### 3.2 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export SPARK_HOME=/usr/app/spark-2.2.3-bin-hadoop2.6 +export PATH=${SPARK_HOME}/bin:$PATH +``` + +使得配置的环境变量立即生效: + +```shell +# source /etc/profile +``` + +### 3.3 集群配置 + +进入 `${SPARK_HOME}/conf` 目录,拷贝配置样本进行修改: + +#### 1. spark-env.sh + +```she + cp spark-env.sh.template spark-env.sh +``` + +```shell +# 配置JDK安装位置 +JAVA_HOME=/usr/java/jdk1.8.0_201 +# 配置hadoop配置文件的位置 +HADOOP_CONF_DIR=/usr/app/hadoop-2.6.0-cdh5.15.2/etc/hadoop +# 配置zookeeper地址 +SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspark.deploy.zookeeper.url=hadoop001:2181,hadoop002:2181,hadoop003:2181 -Dspark.deploy.zookeeper.dir=/spark" +``` + +#### 2. slaves + +``` +cp slaves.template slaves +``` + +配置所有 Woker 节点的位置: + +```properties +hadoop001 +hadoop002 +hadoop003 +``` + +### 3.4 安装包分发 + +将 Spark 的安装包分发到其他服务器,分发后建议在这两台服务器上也配置一下 Spark 的环境变量。 + +```shell +scp -r /usr/app/spark-2.4.0-bin-hadoop2.6/ hadoop002:usr/app/ +scp -r /usr/app/spark-2.4.0-bin-hadoop2.6/ hadoop003:usr/app/ +``` + + + +## 四、启动集群 + +### 4.1 启动ZooKeeper集群 + +分别到三台服务器上启动 ZooKeeper 服务: + +```shell + zkServer.sh start +``` + +### 4.2 启动Hadoop集群 + +```shell +# 启动dfs服务 +start-dfs.sh +# 启动yarn服务 +start-yarn.sh +``` + +### 4.3 启动Spark集群 + +进入 hadoop001 的 ` ${SPARK_HOME}/sbin` 目录下,执行下面命令启动集群。执行命令后,会在 hadoop001 上启动 `Maser` 服务,会在 `slaves` 配置文件中配置的所有节点上启动 `Worker` 服务。 + +```shell +start-all.sh +``` + +分别在 hadoop002 和 hadoop003 上执行下面的命令,启动备用的 `Master` 服务: + +```shell +# ${SPARK_HOME}/sbin 下执行 +start-master.sh +``` + +### 4.4 查看服务 + +查看 Spark 的 Web-UI 页面,端口为 `8080`。此时可以看到 hadoop001 上的 Master 节点处于 `ALIVE` 状态,并有 3 个可用的 `Worker` 节点。 + +
+ +而 hadoop002 和 hadoop003 上的 Master 节点均处于 `STANDBY` 状态,没有可用的 `Worker` 节点。 + +
+ +
+ + + +## 五、验证集群高可用 + +此时可以使用 `kill` 命令杀死 hadoop001 上的 `Master` 进程,此时备用 `Master` 会中会有一个再次成为 ` 主 Master`,我这里是 hadoop002,可以看到 hadoop2 上的 `Master` 经过 `RECOVERING` 后成为了新的主 `Master`,并且获得了全部可以用的 `Workers`。 + +
+ +Hadoop002 上的 `Master` 成为主 `Master`,并获得了全部可以用的 `Workers`。 + +
+ +此时如果你再在 hadoop001 上使用 `start-master.sh` 启动 Master 服务,那么其会作为备用 `Master` 存在。 + +## 六、提交作业 + +和单机环境下的提交到 Yarn 上的命令完全一致,这里以 Spark 内置的计算 Pi 的样例程序为例,提交命令如下: + +```shell +spark-submit \ +--class org.apache.spark.examples.SparkPi \ +--master yarn \ +--deploy-mode client \ +--executor-memory 1G \ +--num-executors 10 \ +/usr/app/spark-2.4.0-bin-hadoop2.6/examples/jars/spark-examples_2.11-2.4.0.jar \ +100 +``` + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..4e3be58 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\345\215\225\346\234\272\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,81 @@ +# Storm单机版本环境搭建 + +### 1. 安装环境要求 + +> you need to install Storm's dependencies on Nimbus and the worker machines. These are: +> +> 1. Java 7+ (Apache Storm 1.x is tested through travis ci against both java 7 and java 8 JDKs) +> 2. Python 2.6.6 (Python 3.x should work too, but is not tested as part of our CI enviornment) + +按照[官方文档](http://storm.apache.org/releases/1.2.2/Setting-up-a-Storm-cluster.html) 的说明:storm 运行依赖于 Java 7+ 和 Python 2.6.6 +,所以需要预先安装这两个软件。由于这两个软件在多个框架中都有依赖,其安装步骤单独整理至 : + ++ [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) + ++ [Linux 环境下 Python 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下Python安装.md) + + + +### 2. 下载并解压 + +下载并解压,官方下载地址:http://storm.apache.org/downloads.html + +```shell +# tar -zxvf apache-storm-1.2.2.tar.gz +``` + +### 3. 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export STORM_HOME=/usr/app/apache-storm-1.2.2 +export PATH=$STORM_HOME/bin:$PATH +``` + +使得配置的环境变量生效: + +```shell +# source /etc/profile +``` + + + +### 4. 启动相关进程 + +因为要启动多个进程,所以统一采用后台进程的方式启动。进入到 `${STORM_HOME}/bin` 目录下,依次执行下面的命令: + +```shell +# 启动zookeeper +nohup sh storm dev-zookeeper & +# 启动主节点 nimbus +nohup sh storm nimbus & +# 启动从节点 supervisor +nohup sh storm supervisor & +# 启动UI界面 ui +nohup sh storm ui & +# 启动日志查看服务 logviewer +nohup sh storm logviewer & +``` + + + +### 5. 验证是否启动成功 + +验证方式一:jps 查看进程: + +```shell +[root@hadoop001 app]# jps +1074 nimbus +1283 Supervisor +620 dev_zookeeper +1485 core +9630 logviewer +``` + +验证方式二: 访问 8080 端口,查看 Web-UI 界面: + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..c461bd7 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Storm\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,167 @@ +# Storm集群环境搭建 + + + + + + + + + +## 一、集群规划 + +这里搭建一个 3 节点的 Storm 集群:三台主机上均部署 `Supervisor` 和 `LogViewer` 服务。同时为了保证高可用,除了在 hadoop001 上部署主 `Nimbus` 服务外,还在 hadoop002 上部署备用的 `Nimbus` 服务。`Nimbus` 服务由 Zookeeper 集群进行协调管理,如果主 `Nimbus` 不可用,则备用 `Nimbus` 会成为新的主 `Nimbus`。 + +
+ +## 二、前置条件 + +Storm 运行依赖于 Java 7+ 和 Python 2.6.6 +,所以需要预先安装这两个软件。同时为了保证高可用,这里我们不采用 Storm 内置的 Zookeeper,而采用外置的 Zookeeper 集群。由于这三个软件在多个框架中都有依赖,其安装步骤单独整理至 : + +- [Linux 环境下 JDK 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md) +- [Linux 环境下 Python 安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下Python安装.md) +- [Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) + + + +## 三、集群搭建 + +### 1. 下载并解压 + +下载安装包,之后进行解压。官方下载地址:http://storm.apache.org/downloads.html + +```shell +# 解压 +tar -zxvf apache-storm-1.2.2.tar.gz + +``` + +### 2. 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export STORM_HOME=/usr/app/apache-storm-1.2.2 +export PATH=$STORM_HOME/bin:$PATH +``` + +使得配置的环境变量生效: + +```shell +# source /etc/profile +``` + +### 3. 集群配置 + +修改 `${STORM_HOME}/conf/storm.yaml` 文件,配置如下: + +```yaml +# Zookeeper集群的主机列表 +storm.zookeeper.servers: + - "hadoop001" + - "hadoop002" + - "hadoop003" + +# Nimbus的节点列表 +nimbus.seeds: ["hadoop001","hadoop002"] + +# Nimbus和Supervisor需要使用本地磁盘上来存储少量状态(如jar包,配置文件等) +storm.local.dir: "/home/storm" + +# workers进程的端口,每个worker进程会使用一个端口来接收消息 +supervisor.slots.ports: + - 6700 + - 6701 + - 6702 + - 6703 +``` + +`supervisor.slots.ports` 参数用来配置 workers 进程接收消息的端口,默认每个 supervisor 节点上会启动 4 个 worker,当然你也可以按照自己的需要和服务器性能进行设置,假设只想启动 2 个 worker 的话,此处配置 2 个端口即可。 + +### 4. 安装包分发 + +将 Storm 的安装包分发到其他服务器,分发后建议在这两台服务器上也配置一下 Storm 的环境变量。 + +```shell +scp -r /usr/app/apache-storm-1.2.2/ root@hadoop002:/usr/app/ +scp -r /usr/app/apache-storm-1.2.2/ root@hadoop003:/usr/app/ +``` + + + +## 四. 启动集群 + +### 4.1 启动ZooKeeper集群 + +分别到三台服务器上启动 ZooKeeper 服务: + +```shell + zkServer.sh start +``` + +### 4.2 启动Storm集群 + +因为要启动多个进程,所以统一采用后台进程的方式启动。进入到 `${STORM_HOME}/bin` 目录下,执行下面的命令: + +**hadoop001 & hadoop002 :** + +```shell +# 启动主节点 nimbus +nohup sh storm nimbus & +# 启动从节点 supervisor +nohup sh storm supervisor & +# 启动UI界面 ui +nohup sh storm ui & +# 启动日志查看服务 logviewer +nohup sh storm logviewer & +``` + +**hadoop003 :** + +hadoop003 上只需要启动 `supervisor` 服务和 `logviewer` 服务: + +```shell +# 启动从节点 supervisor +nohup sh storm supervisor & +# 启动日志查看服务 logviewer +nohup sh storm logviewer & +``` + + + +### 4.3 查看集群 + +使用 `jps` 查看进程,三台服务器的进程应该分别如下: + +
+ + +
+ +访问 hadoop001 或 hadoop002 的 `8080` 端口,界面如下。可以看到有一主一备 2 个 `Nimbus` 和 3 个 `Supervisor`,并且每个 `Supervisor` 有四个 `slots`,即四个可用的 `worker` 进程,此时代表集群已经搭建成功。 + +
+ + +## 五、高可用验证 + +这里手动模拟主 `Nimbus` 异常的情况,在 hadoop001 上使用 `kill` 命令杀死 `Nimbus` 的线程,此时可以看到 hadoop001 上的 `Nimbus` 已经处于 `offline` 状态,而 hadoop002 上的 `Nimbus` 则成为新的 `Leader`。 + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Zookeeper\345\215\225\346\234\272\347\216\257\345\242\203\345\222\214\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Zookeeper\345\215\225\346\234\272\347\216\257\345\242\203\345\222\214\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" new file mode 100644 index 0000000..a0b75a9 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/Zookeeper\345\215\225\346\234\272\347\216\257\345\242\203\345\222\214\351\233\206\347\276\244\347\216\257\345\242\203\346\220\255\345\273\272.md" @@ -0,0 +1,187 @@ +# Zookeeper单机环境和集群环境搭建 + + + + +## 一、单机环境搭建 + +### 1.1 下载 + +下载对应版本 Zookeeper,这里我下载的版本 `3.4.14`。官方下载地址:https://archive.apache.org/dist/zookeeper/ + +```shell +# wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz +``` + +### 1.2 解压 + +```shell +# tar -zxvf zookeeper-3.4.14.tar.gz +``` + +### 1.3 配置环境变量 + +```shell +# vim /etc/profile +``` + +添加环境变量: + +```shell +export ZOOKEEPER_HOME=/usr/app/zookeeper-3.4.14 +export PATH=$ZOOKEEPER_HOME/bin:$PATH +``` + +使得配置的环境变量生效: + +```shell +# source /etc/profile +``` + +### 1.4 修改配置 + +进入安装目录的 `conf/` 目录下,拷贝配置样本并进行修改: + +``` +# cp zoo_sample.cfg zoo.cfg +``` + +指定数据存储目录和日志文件目录(目录不用预先创建,程序会自动创建),修改后完整配置如下: + +```properties +# 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. +dataDir=/usr/local/zookeeper/data +dataLogDir=/usr/local/zookeeper/log +# 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 +``` + +>配置参数说明: +> +>- **tickTime**:用于计算的基础时间单元。比如 session 超时:N*tickTime; +>- **initLimit**:用于集群,允许从节点连接并同步到 master 节点的初始化连接时间,以 tickTime 的倍数来表示; +>- **syncLimit**:用于集群, master 主节点与从节点之间发送消息,请求和应答时间长度(心跳机制); +>- **dataDir**:数据存储位置; +>- **dataLogDir**:日志目录; +>- **clientPort**:用于客户端连接的端口,默认 2181 + + + +### 1.5 启动 + +由于已经配置过环境变量,直接使用下面命令启动即可: + +``` +zkServer.sh start +``` + +### 1.6 验证 + +使用 JPS 验证进程是否已经启动,出现 `QuorumPeerMain` 则代表启动成功。 + +```shell +[root@hadoop001 bin]# jps +3814 QuorumPeerMain +``` + + + +## 二、集群环境搭建 + +为保证集群高可用,Zookeeper 集群的节点数最好是奇数,最少有三个节点,所以这里演示搭建一个三个节点的集群。这里我使用三台主机进行搭建,主机名分别为 hadoop001,hadoop002,hadoop003。 + +### 2.1 修改配置 + +解压一份 zookeeper 安装包,修改其配置文件 `zoo.cfg`,内容如下。之后使用 scp 命令将安装包分发到三台服务器上: + +```shell +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/usr/local/zookeeper-cluster/data/ +dataLogDir=/usr/local/zookeeper-cluster/log/ +clientPort=2181 + +# server.1 这个1是服务器的标识,可以是任意有效数字,标识这是第几个服务器节点,这个标识要写到dataDir目录下面myid文件里 +# 指名集群间通讯端口和选举端口 +server.1=hadoop001:2287:3387 +server.2=hadoop002:2287:3387 +server.3=hadoop003:2287:3387 +``` + +### 2.2 标识节点 + +分别在三台主机的 `dataDir` 目录下新建 `myid` 文件,并写入对应的节点标识。Zookeeper 集群通过 `myid` 文件识别集群节点,并通过上文配置的节点通信端口和选举端口来进行节点通信,选举出 Leader 节点。 + +创建存储目录: + +```shell +# 三台主机均执行该命令 +mkdir -vp /usr/local/zookeeper-cluster/data/ +``` + +创建并写入节点标识到 `myid` 文件: + +```shell +# hadoop001主机 +echo "1" > /usr/local/zookeeper-cluster/data/myid +# hadoop002主机 +echo "2" > /usr/local/zookeeper-cluster/data/myid +# hadoop003主机 +echo "3" > /usr/local/zookeeper-cluster/data/myid +``` + +### 2.3 启动集群 + +分别在三台主机上,执行如下命令启动服务: + +```shell +/usr/app/zookeeper-cluster/zookeeper/bin/zkServer.sh start +``` + +### 2.4 集群验证 + +启动后使用 `zkServer.sh status` 查看集群各个节点状态。如图所示:三个节点进程均启动成功,并且 hadoop002 为 leader 节点,hadoop001 和 hadoop003 为 follower 节点。 + +
+ +
+ +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Hadoop\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Hadoop\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" new file mode 100644 index 0000000..0912b7d --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Hadoop\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" @@ -0,0 +1,514 @@ +# 基于ZooKeeper搭建Hadoop高可用集群 + + + + + +## 一、高可用简介 + +Hadoop 高可用 (High Availability) 分为 HDFS 高可用和 YARN 高可用,两者的实现基本类似,但 HDFS NameNode 对数据存储及其一致性的要求比 YARN ResourceManger 高得多,所以它的实现也更加复杂,故下面先进行讲解: + +### 1.1 高可用整体架构 + +HDFS 高可用架构如下: + +
+ +> *图片引用自:https://www.edureka.co/blog/how-to-set-up-hadoop-cluster-with-hdfs-high-availability/* + +HDFS 高可用架构主要由以下组件所构成: + ++ **Active NameNode 和 Standby NameNode**:两台 NameNode 形成互备,一台处于 Active 状态,为主 NameNode,另外一台处于 Standby 状态,为备 NameNode,只有主 NameNode 才能对外提供读写服务。 + ++ **主备切换控制器 ZKFailoverController**:ZKFailoverController 作为独立的进程运行,对 NameNode 的主备切换进行总体控制。ZKFailoverController 能及时检测到 NameNode 的健康状况,在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换,当然 NameNode 目前也支持不依赖于 Zookeeper 的手动主备切换。 + ++ **Zookeeper 集群**:为主备切换控制器提供主备选举支持。 + ++ **共享存储系统**:共享存储系统是实现 NameNode 的高可用最为关键的部分,共享存储系统保存了 NameNode 在运行过程中所产生的 HDFS 的元数据。主 NameNode 和 NameNode 通过共享存储系统实现元数据同步。在进行主备切换的时候,新的主 NameNode 在确认元数据完全同步之后才能继续对外提供服务。 + ++ **DataNode 节点**:除了通过共享存储系统共享 HDFS 的元数据信息之外,主 NameNode 和备 NameNode 还需要共享 HDFS 的数据块和 DataNode 之间的映射关系。DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。 + +### 1.2 基于 QJM 的共享存储系统的数据同步机制分析 + +目前 Hadoop 支持使用 Quorum Journal Manager (QJM) 或 Network File System (NFS) 作为共享的存储系统,这里以 QJM 集群为例进行说明:Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog,当 Active NameNode 宕机后, Standby NameNode 在确认元数据完全同步之后就可以对外提供服务。 + +需要说明的是向 JournalNode 集群写入 EditLog 是遵循 “过半写入则成功” 的策略,所以你至少要有 3 个 JournalNode 节点,当然你也可以继续增加节点数量,但是应该保证节点总数是奇数。同时如果有 2N+1 台 JournalNode,那么根据过半写的原则,最多可以容忍有 N 台 JournalNode 节点挂掉。 + +
+ +### 1.3 NameNode 主备切换 + +NameNode 实现主备切换的流程下图所示: + +
+1. HealthMonitor 初始化完成之后会启动内部的线程来定时调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法,对 NameNode 的健康状态进行检测。 +2. HealthMonitor 如果检测到 NameNode 的健康状态发生变化,会回调 ZKFailoverController 注册的相应方法进行处理。 +3. 如果 ZKFailoverController 判断需要进行主备切换,会首先使用 ActiveStandbyElector 来进行自动的主备选举。 +4. ActiveStandbyElector 与 Zookeeper 进行交互完成自动的主备选举。 +5. ActiveStandbyElector 在主备选举完成后,会回调 ZKFailoverController 的相应方法来通知当前的 NameNode 成为主 NameNode 或备 NameNode。 +6. ZKFailoverController 调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法将 NameNode 转换为 Active 状态或 Standby 状态。 + + +### 1.4 YARN高可用 + +YARN ResourceManager 的高可用与 HDFS NameNode 的高可用类似,但是 ResourceManager 不像 NameNode ,没有那么多的元数据信息需要维护,所以它的状态信息可以直接写到 Zookeeper 上,并依赖 Zookeeper 来进行主备选举。 + + + +
+ + +## 二、集群规划 + +按照高可用的设计目标:需要保证至少有两个 NameNode (一主一备) 和 两个 ResourceManager (一主一备) ,同时为满足“过半写入则成功”的原则,需要至少要有 3 个 JournalNode 节点。这里使用三台主机进行搭建,集群规划如下: + +
+ + +## 三、前置条件 + ++ 所有服务器都安装有 JDK,安装步骤可以参见:[Linux 下 JDK 的安装](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Linux下JDK安装.md); ++ 搭建好 ZooKeeper 集群,搭建步骤可以参见:[Zookeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md) ++ 所有服务器之间都配置好 SSH 免密登录。 + + + +## 四、集群配置 + +### 4.1 下载并解压 + +下载 Hadoop。这里我下载的是 CDH 版本 Hadoop,下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ + +```shell +# tar -zvxf hadoop-2.6.0-cdh5.15.2.tar.gz +``` + +### 4.2 配置环境变量 + +编辑 `profile` 文件: + +```shell +# vim /etc/profile +``` + +增加如下配置: + +``` +export HADOOP_HOME=/usr/app/hadoop-2.6.0-cdh5.15.2 +export PATH=${HADOOP_HOME}/bin:$PATH +``` + +执行 `source` 命令,使得配置立即生效: + +```shell +# source /etc/profile +``` + +### 4.3 修改配置 + +进入 `${HADOOP_HOME}/etc/hadoop` 目录下,修改配置文件。各个配置文件内容如下: + +#### 1. hadoop-env.sh + +```shell +# 指定JDK的安装位置 +export JAVA_HOME=/usr/java/jdk1.8.0_201/ +``` + +#### 2. core-site.xml + +```xml + + + + fs.defaultFS + hdfs://hadoop001:8020 + + + + hadoop.tmp.dir + /home/hadoop/tmp + + + + ha.zookeeper.quorum + hadoop001:2181,hadoop002:2181,hadoop002:2181 + + + + ha.zookeeper.session-timeout.ms + 10000 + + +``` + +#### 3. hdfs-site.xml + +```xml + + + + dfs.replication + 3 + + + + dfs.namenode.name.dir + /home/hadoop/namenode/data + + + + dfs.datanode.data.dir + /home/hadoop/datanode/data + + + + dfs.nameservices + mycluster + + + + dfs.ha.namenodes.mycluster + nn1,nn2 + + + + dfs.namenode.rpc-address.mycluster.nn1 + hadoop001:8020 + + + + dfs.namenode.rpc-address.mycluster.nn2 + hadoop002:8020 + + + + dfs.namenode.http-address.mycluster.nn1 + hadoop001:50070 + + + + dfs.namenode.http-address.mycluster.nn2 + hadoop002:50070 + + + + dfs.namenode.shared.edits.dir + qjournal://hadoop001:8485;hadoop002:8485;hadoop003:8485/mycluster + + + + dfs.journalnode.edits.dir + /home/hadoop/journalnode/data + + + + dfs.ha.fencing.methods + sshfence + + + + dfs.ha.fencing.ssh.private-key-files + /root/.ssh/id_rsa + + + + dfs.ha.fencing.ssh.connect-timeout + 30000 + + + + dfs.client.failover.proxy.provider.mycluster + org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider + + + + dfs.ha.automatic-failover.enabled + true + + +``` + +#### 4. yarn-site.xml + +```xml + + + + yarn.nodemanager.aux-services + mapreduce_shuffle + + + + yarn.log-aggregation-enable + true + + + + yarn.log-aggregation.retain-seconds + 86400 + + + + yarn.resourcemanager.ha.enabled + true + + + + yarn.resourcemanager.cluster-id + my-yarn-cluster + + + + yarn.resourcemanager.ha.rm-ids + rm1,rm2 + + + + yarn.resourcemanager.hostname.rm1 + hadoop002 + + + + yarn.resourcemanager.hostname.rm2 + hadoop003 + + + + yarn.resourcemanager.webapp.address.rm1 + hadoop002:8088 + + + + yarn.resourcemanager.webapp.address.rm2 + hadoop003:8088 + + + + yarn.resourcemanager.zk-address + hadoop001:2181,hadoop002:2181,hadoop003:2181 + + + + yarn.resourcemanager.recovery.enabled + true + + + + yarn.resourcemanager.store.class + org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore + + +``` + +#### 5. mapred-site.xml + +```xml + + + + mapreduce.framework.name + yarn + + +``` + +#### 5. slaves + +配置所有从属节点的主机名或 IP 地址,每行一个。所有从属节点上的 `DataNode` 服务和 `NodeManager` 服务都会被启动。 + +```properties +hadoop001 +hadoop002 +hadoop003 +``` + +### 4.4 分发程序 + +将 Hadoop 安装包分发到其他两台服务器,分发后建议在这两台服务器上也配置一下 Hadoop 的环境变量。 + +```shell +# 将安装包分发到hadoop002 +scp -r /usr/app/hadoop-2.6.0-cdh5.15.2/ hadoop002:/usr/app/ +# 将安装包分发到hadoop003 +scp -r /usr/app/hadoop-2.6.0-cdh5.15.2/ hadoop003:/usr/app/ +``` + + + +## 五、启动集群 + +### 5.1 启动ZooKeeper + +分别到三台服务器上启动 ZooKeeper 服务: + +```ssh + zkServer.sh start +``` + +### 5.2 启动Journalnode + +分别到三台服务器的的 `${HADOOP_HOME}/sbin` 目录下,启动 `journalnode` 进程: + +```shell +hadoop-daemon.sh start journalnode +``` + +### 5.3 初始化NameNode + +在 `hadop001` 上执行 `NameNode` 初始化命令: + +``` +hdfs namenode -format +``` + +执行初始化命令后,需要将 `NameNode` 元数据目录的内容,复制到其他未格式化的 `NameNode` 上。元数据存储目录就是我们在 `hdfs-site.xml` 中使用 `dfs.namenode.name.dir` 属性指定的目录。这里我们需要将其复制到 `hadoop002` 上: + +```shell + scp -r /home/hadoop/namenode/data hadoop002:/home/hadoop/namenode/ +``` + +### 5.4 初始化HA状态 + +在任意一台 `NameNode` 上使用以下命令来初始化 ZooKeeper 中的 HA 状态: + +```shell +hdfs zkfc -formatZK +``` + +### 5.5 启动HDFS + +进入到 `hadoop001` 的 `${HADOOP_HOME}/sbin` 目录下,启动 HDFS。此时 `hadoop001` 和 `hadoop002` 上的 `NameNode` 服务,和三台服务器上的 `DataNode` 服务都会被启动: + +```shell +start-dfs.sh +``` + +### 5.6 启动YARN + +进入到 `hadoop002` 的 `${HADOOP_HOME}/sbin` 目录下,启动 YARN。此时 `hadoop002` 上的 `ResourceManager` 服务,和三台服务器上的 `NodeManager` 服务都会被启动: + +```SHEll +start-yarn.sh +``` + +需要注意的是,这个时候 `hadoop003` 上的 `ResourceManager` 服务通常是没有启动的,需要手动启动: + +```shell +yarn-daemon.sh start resourcemanager +``` + +## 六、查看集群 + +### 6.1 查看进程 + +成功启动后,每台服务器上的进程应该如下: + +```shell +[root@hadoop001 sbin]# jps +4512 DFSZKFailoverController +3714 JournalNode +4114 NameNode +3668 QuorumPeerMain +5012 DataNode +4639 NodeManager + + +[root@hadoop002 sbin]# jps +4499 ResourceManager +4595 NodeManager +3465 QuorumPeerMain +3705 NameNode +3915 DFSZKFailoverController +5211 DataNode +3533 JournalNode + + +[root@hadoop003 sbin]# jps +3491 JournalNode +3942 NodeManager +4102 ResourceManager +4201 DataNode +3435 QuorumPeerMain +``` + + + +### 6.2 查看Web UI + +HDFS 和 YARN 的端口号分别为 `50070` 和 `8080`,界面应该如下: + +此时 hadoop001 上的 `NameNode` 处于可用状态: + +
+而 hadoop002 上的 `NameNode` 则处于备用状态: + +
+ +
+
+ +hadoop002 上的 `ResourceManager` 处于可用状态: + +
+ +
+
+ +hadoop003 上的 `ResourceManager` 则处于备用状态: + +
+ +
+
+ +同时界面上也有 `Journal Manager` 的相关信息: + +
+ +
+## 七、集群的二次启动 + +上面的集群初次启动涉及到一些必要初始化操作,所以过程略显繁琐。但是集群一旦搭建好后,想要再次启用它是比较方便的,步骤如下(首选需要确保 ZooKeeper 集群已经启动): + +在 ` hadoop001` 启动 HDFS,此时会启动所有与 HDFS 高可用相关的服务,包括 NameNode,DataNode 和 JournalNode: + +```shell +start-dfs.sh +``` + +在 `hadoop002` 启动 YARN: + +```SHEll +start-yarn.sh +``` + +这个时候 `hadoop003` 上的 `ResourceManager` 服务通常还是没有启动的,需要手动启动: + +```shell +yarn-daemon.sh start resourcemanager +``` + + + + + +## 参考资料 + +以上搭建步骤主要参考自官方文档: + ++ [HDFS High Availability Using the Quorum Journal Manager](https://hadoop.apache.org/docs/r3.1.2/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html) ++ [ResourceManager High Availability](https://hadoop.apache.org/docs/r3.1.2/hadoop-yarn/hadoop-yarn-site/ResourceManagerHA.html) + +关于 Hadoop 高可用原理的详细分析,推荐阅读: + +[Hadoop NameNode 高可用 (High Availability) 实现解析](https://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop-name-node/index.html) + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Kafka\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Kafka\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" new file mode 100644 index 0000000..578353e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\345\237\272\344\272\216Zookeeper\346\220\255\345\273\272Kafka\351\253\230\345\217\257\347\224\250\351\233\206\347\276\244.md" @@ -0,0 +1,239 @@ +# 基于Zookeeper搭建Kafka高可用集群 + + + +## 一、Zookeeper集群搭建 + +为保证集群高可用,Zookeeper 集群的节点数最好是奇数,最少有三个节点,所以这里搭建一个三个节点的集群。 + +### 1.1 下载 & 解压 + +下载对应版本 Zookeeper,这里我下载的版本 `3.4.14`。官方下载地址:https://archive.apache.org/dist/zookeeper/ + +```shell +# 下载 +wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz +# 解压 +tar -zxvf zookeeper-3.4.14.tar.gz +``` + +### 1.2 修改配置 + +拷贝三份 zookeeper 安装包。分别进入安装目录的 `conf` 目录,拷贝配置样本 `zoo_sample.cfg ` 为 `zoo.cfg` 并进行修改,修改后三份配置文件内容分别如下: + +zookeeper01 配置: + +```shell +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/usr/local/zookeeper-cluster/data/01 +dataLogDir=/usr/local/zookeeper-cluster/log/01 +clientPort=2181 + +# server.1 这个1是服务器的标识,可以是任意有效数字,标识这是第几个服务器节点,这个标识要写到dataDir目录下面myid文件里 +# 指名集群间通讯端口和选举端口 +server.1=127.0.0.1:2287:3387 +server.2=127.0.0.1:2288:3388 +server.3=127.0.0.1:2289:3389 +``` + +> 如果是多台服务器,则集群中每个节点通讯端口和选举端口可相同,IP 地址修改为每个节点所在主机 IP 即可。 + +zookeeper02 配置,与 zookeeper01 相比,只有 `dataLogDir` 和 `dataLogDir` 不同: + +```shell +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/usr/local/zookeeper-cluster/data/02 +dataLogDir=/usr/local/zookeeper-cluster/log/02 +clientPort=2182 + +server.1=127.0.0.1:2287:3387 +server.2=127.0.0.1:2288:3388 +server.3=127.0.0.1:2289:3389 +``` + +zookeeper03 配置,与 zookeeper01,02 相比,也只有 `dataLogDir` 和 `dataLogDir` 不同: + +```shell +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/usr/local/zookeeper-cluster/data/03 +dataLogDir=/usr/local/zookeeper-cluster/log/03 +clientPort=2183 + +server.1=127.0.0.1:2287:3387 +server.2=127.0.0.1:2288:3388 +server.3=127.0.0.1:2289:3389 +``` + +> 配置参数说明: +> +> - **tickTime**:用于计算的基础时间单元。比如 session 超时:N*tickTime; +> - **initLimit**:用于集群,允许从节点连接并同步到 master 节点的初始化连接时间,以 tickTime 的倍数来表示; +> - **syncLimit**:用于集群, master 主节点与从节点之间发送消息,请求和应答时间长度(心跳机制); +> - **dataDir**:数据存储位置; +> - **dataLogDir**:日志目录; +> - **clientPort**:用于客户端连接的端口,默认 2181 + + + +### 1.3 标识节点 + +分别在三个节点的数据存储目录下新建 `myid` 文件,并写入对应的节点标识。Zookeeper 集群通过 `myid` 文件识别集群节点,并通过上文配置的节点通信端口和选举端口来进行节点通信,选举出 leader 节点。 + +创建存储目录: + +```shell +# dataDir +mkdir -vp /usr/local/zookeeper-cluster/data/01 +# dataDir +mkdir -vp /usr/local/zookeeper-cluster/data/02 +# dataDir +mkdir -vp /usr/local/zookeeper-cluster/data/03 +``` + +创建并写入节点标识到 `myid` 文件: + +```shell +#server1 +echo "1" > /usr/local/zookeeper-cluster/data/01/myid +#server2 +echo "2" > /usr/local/zookeeper-cluster/data/02/myid +#server3 +echo "3" > /usr/local/zookeeper-cluster/data/03/myid +``` + +### 1.4 启动集群 + +分别启动三个节点: + +```shell +# 启动节点1 +/usr/app/zookeeper-cluster/zookeeper01/bin/zkServer.sh start +# 启动节点2 +/usr/app/zookeeper-cluster/zookeeper02/bin/zkServer.sh start +# 启动节点3 +/usr/app/zookeeper-cluster/zookeeper03/bin/zkServer.sh start +``` + +### 1.5 集群验证 + +使用 jps 查看进程,并且使用 `zkServer.sh status` 查看集群各个节点状态。如图三个节点进程均启动成功,并且两个节点为 follower 节点,一个节点为 leader 节点。 + +
+ + + +## 二、Kafka集群搭建 + +### 2.1 下载解压 + +Kafka 安装包官方下载地址:http://kafka.apache.org/downloads ,本用例下载的版本为 `2.2.0`,下载命令: + +```shell +# 下载 +wget https://www-eu.apache.org/dist/kafka/2.2.0/kafka_2.12-2.2.0.tgz +# 解压 +tar -xzf kafka_2.12-2.2.0.tgz +``` + +>这里 j 解释一下 kafka 安装包的命名规则:以 `kafka_2.12-2.2.0.tgz` 为例,前面的 2.12 代表 Scala 的版本号(Kafka 采用 Scala 语言进行开发),后面的 2.2.0 则代表 Kafka 的版本号。 + +### 2.2 拷贝配置文件 + +进入解压目录的 ` config` 目录下 ,拷贝三份配置文件: + +```shell +# cp server.properties server-1.properties +# cp server.properties server-2.properties +# cp server.properties server-3.properties +``` + +### 2.3 修改配置 + +分别修改三份配置文件中的部分配置,如下: + +server-1.properties: + +```properties +# The id of the broker. 集群中每个节点的唯一标识 +broker.id=0 +# 监听地址 +listeners=PLAINTEXT://hadoop001:9092 +# 数据的存储位置 +log.dirs=/usr/local/kafka-logs/00 +# Zookeeper连接地址 +zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183 +``` + +server-2.properties: + +```properties +broker.id=1 +listeners=PLAINTEXT://hadoop001:9093 +log.dirs=/usr/local/kafka-logs/01 +zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183 +``` + +server-3.properties: + +```properties +broker.id=2 +listeners=PLAINTEXT://hadoop001:9094 +log.dirs=/usr/local/kafka-logs/02 +zookeeper.connect=hadoop001:2181,hadoop001:2182,hadoop001:2183 +``` + +这里需要说明的是 `log.dirs` 指的是数据日志的存储位置,确切的说,就是分区数据的存储位置,而不是程序运行日志的位置。程序运行日志的位置是通过同一目录下的 `log4j.properties` 进行配置的。 + +### 2.4 启动集群 + +分别指定不同配置文件,启动三个 Kafka 节点。启动后可以使用 jps 查看进程,此时应该有三个 zookeeper 进程和三个 kafka 进程。 + +```shell +bin/kafka-server-start.sh config/server-1.properties +bin/kafka-server-start.sh config/server-2.properties +bin/kafka-server-start.sh config/server-3.properties +``` + +### 2.5 创建测试主题 + +创建测试主题: + +```shell +bin/kafka-topics.sh --create --bootstrap-server hadoop001:9092 \ + --replication-factor 3 \ + --partitions 1 --topic my-replicated-topic +``` + +创建后可以使用以下命令查看创建的主题信息: + +```shell +bin/kafka-topics.sh --describe --bootstrap-server hadoop001:9092 --topic my-replicated-topic +``` + +
+ + + +可以看到分区 0 的有 0,1,2 三个副本,且三个副本都是可用副本,都在 ISR(in-sync Replica 同步副本) 列表中,其中 1 为首领副本,此时代表集群已经搭建成功。 + + + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\350\231\232\346\213\237\346\234\272\351\235\231\346\200\201IP\345\217\212\345\244\232IP\351\205\215\347\275\256.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\350\231\232\346\213\237\346\234\272\351\235\231\346\200\201IP\345\217\212\345\244\232IP\351\205\215\347\275\256.md" new file mode 100644 index 0000000..bd426cd --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/installation/\350\231\232\346\213\237\346\234\272\351\235\231\346\200\201IP\345\217\212\345\244\232IP\351\205\215\347\275\256.md" @@ -0,0 +1,118 @@ +# 虚拟机静态IP及多IP配置 + + + + + +## 一、虚拟机静态IP配置 + +### 1. 编辑网络配置文件 + +```shell +# vim /etc/sysconfig/network-scripts/ifcfg-enp0s3 +``` + +添加如下网络配置: + ++ IPADDR 需要和宿主机同一个网段; ++ GATEWAY 保持和宿主机一致; + +```properties +BOOTPROTO=static +IPADDR=192.168.0.107 +NETMASK=255.255.255.0 +GATEWAY=192.168.0.1 +DNS1=192.168.0.1 +ONBOOT=yes +``` + +我的主机配置: + +
+ +修改后完整配置如下: + +```properties +TYPE=Ethernet +PROXY_METHOD=none +BROWSER_ONLY=no +BOOTPROTO=static +IPADDR=192.168.0.107 +NETMASK=255.255.255.0 +GATEWAY=192.168.0.1 +BROADCAST=192.168.0.255 +DNS1=192.168.0.1 +DEFROUTE=yes +IPV4_FAILURE_FATAL=no +IPV6INIT=yes +IPV6_AUTOCONF=yes +IPV6_DEFROUTE=yes +IPV6_FAILURE_FATAL=no +IPV6_ADDR_GEN_MODE=stable-privacy +NAME=enp0s3 +UUID=03d45df1-8514-4774-9b47-fddd6b9d9fca +DEVICE=enp0s3 +ONBOOT=yes +``` + +### 2. 重启网络服务 + +```shell +# systemctl restart network +``` + + + +## 二、虚拟机多个静态IP配置 + +如果一台虚拟机需要经常在不同网络环境使用,可以配置多个静态 IP。 + +### 1. 配置多网卡 + +这里我是用的虚拟机是 virtualBox,开启多网卡配置方式如下: + +
+ +### 2. 查看网卡名称 + +使用 `ifconfig`,查看第二块网卡名称,这里我的名称为 `enp0s8`: + +
+ +### 3. 配置第二块网卡 + +开启多网卡后并不会自动生成配置文件,需要拷贝 `ifcfg-enp0s3` 进行修改: + +```shell +# cp ifcfg-enp0s3 ifcfg-enp0s8 +``` + +静态 IP 配置方法如上,这里不再赘述。除了静态 IP 参数外,以下三个参数还需要修改,UUID 必须与 `ifcfg-enp0s3` 中的不一样: + +```properties +NAME=enp0s8 +UUID=03d45df1-8514-4774-9b47-fddd6b9d9fcb +DEVICE=enp0s8 +``` + +### 4. 重启网络服务器 + +```shell +# systemctl restart network +``` + +### 5. 使用说明 + +使用时只需要根据所处的网络环境,勾选对应的网卡即可,不使用的网卡尽量不要勾选启动。 + +
diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240\350\267\257\347\272\277.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240\350\267\257\347\272\277.md" new file mode 100644 index 0000000..1cd0045 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240\350\267\257\347\272\277.md" @@ -0,0 +1,169 @@ +# 大数据学习路线 + + + +## 一、大数据处理流程 + +
+上图是一个简化的大数据处理流程图,大数据处理的主要流程包括数据收集、数据存储、数据处理、数据应用等主要环节。下面我们逐一对各个环节所需要的技术栈进行讲解: + +### 1.1 数据收集 + +大数据处理的第一步是数据的收集。现在的中大型项目通常采用微服务架构进行分布式部署,所以数据的采集需要在多台服务器上进行,且采集过程不能影响正常业务的开展。基于这种需求,就衍生了多种日志收集工具,如 Flume 、Logstash、Kibana 等,它们都能通过简单的配置完成复杂的数据收集和数据聚合。 + +### 1.2 数据存储 + +收集到数据后,下一个问题就是:数据该如何进行存储?通常大家最为熟知是 MySQL、Oracle 等传统的关系型数据库,它们的优点是能够快速存储结构化的数据,并支持随机访问。但大数据的数据结构通常是半结构化(如日志数据)、甚至是非结构化的(如视频、音频数据),为了解决海量半结构化和非结构化数据的存储,衍生了 Hadoop HDFS 、KFS、GFS 等分布式文件系统,它们都能够支持结构化、半结构和非结构化数据的存储,并可以通过增加机器进行横向扩展。 + +分布式文件系统完美地解决了海量数据存储的问题,但是一个优秀的数据存储系统需要同时考虑数据存储和访问两方面的问题,比如你希望能够对数据进行随机访问,这是传统的关系型数据库所擅长的,但却不是分布式文件系统所擅长的,那么有没有一种存储方案能够同时兼具分布式文件系统和关系型数据库的优点,基于这种需求,就产生了 HBase、MongoDB。 + +### 1.3 数据分析 + +大数据处理最重要的环节就是数据分析,数据分析通常分为两种:批处理和流处理。 + ++ **批处理**:对一段时间内海量的离线数据进行统一的处理,对应的处理框架有 Hadoop MapReduce、Spark、Flink 等; ++ **流处理**:对运动中的数据进行处理,即在接收数据的同时就对其进行处理,对应的处理框架有 Storm、Spark Streaming、Flink Streaming 等。 + +批处理和流处理各有其适用的场景,时间不敏感或者硬件资源有限,可以采用批处理;时间敏感和及时性要求高就可以采用流处理。随着服务器硬件的价格越来越低和大家对及时性的要求越来越高,流处理越来越普遍,如股票价格预测和电商运营数据分析等。 + +上面的框架都是需要通过编程来进行数据分析,那么如果你不是一个后台工程师,是不是就不能进行数据的分析了?当然不是,大数据是一个非常完善的生态圈,有需求就有解决方案。为了能够让熟悉 SQL 的人员也能够进行数据的分析,查询分析框架应运而生,常用的有 Hive 、Spark SQL 、Flink SQL、 Pig、Phoenix 等。这些框架都能够使用标准的 SQL 或者 类 SQL 语法灵活地进行数据的查询分析。这些 SQL 经过解析优化后转换为对应的作业程序来运行,如 Hive 本质上就是将 SQL 转换为 MapReduce 作业,Spark SQL 将 SQL 转换为一系列的 RDDs 和转换关系(transformations),Phoenix 将 SQL 查询转换为一个或多个 HBase Scan。 + +### 1.4 数据应用 + +数据分析完成后,接下来就是数据应用的范畴,这取决于你实际的业务需求。比如你可以将数据进行可视化展现,或者将数据用于优化你的推荐算法,这种运用现在很普遍,比如短视频个性化推荐、电商商品推荐、头条新闻推荐等。当然你也可以将数据用于训练你的机器学习模型,这些都属于其他领域的范畴,都有着对应的框架和技术栈进行处理,这里就不一一赘述。 + +### 1.5 其他框架 + +上面是一个标准的大数据处理流程所用到的技术框架。但是实际的大数据处理流程比上面复杂很多,针对大数据处理中的各种复杂问题分别衍生了各类框架: + ++ 单机的处理能力都是存在瓶颈的,所以大数据框架都是采用集群模式进行部署,为了更方便的进行集群的部署、监控和管理,衍生了 Ambari、Cloudera Manager 等集群管理工具; ++ 想要保证集群高可用,需要用到 ZooKeeper ,ZooKeeper 是最常用的分布式协调服务,它能够解决大多数集群问题,包括首领选举、失败恢复、元数据存储及其一致性保证。同时针对集群资源管理的需求,又衍生了 Hadoop YARN ; ++ 复杂大数据处理的另外一个显著的问题是,如何调度多个复杂的并且彼此之间存在依赖关系的作业?基于这种需求,产生了 Azkaban 和 Oozie 等工作流调度框架; ++ 大数据流处理中使用的比较多的另外一个框架是 Kafka,它可以用于消峰,避免在秒杀等场景下并发数据对流处理程序造成冲击; ++ 另一个常用的框架是 Sqoop ,主要是解决了数据迁移的问题,它能够通过简单的命令将关系型数据库中的数据导入到 HDFS 、Hive 或 HBase 中,或者从 HDFS 、Hive 导出到关系型数据库上。 + +## 二、学习路线 + +介绍完大数据框架,接着就可以介绍其对应的学习路线了,主要分为以下几个方面: + +### 2.1 语言基础 + +#### 1. Java + +大数据框架大多采用 Java 语言进行开发,并且几乎全部的框架都会提供 Java API 。Java 是目前比较主流的后台开发语言,所以网上免费的学习资源也比较多。如果你习惯通过书本进行学习,这里推荐以下入门书籍: + ++ [《Java 编程的逻辑》](https://book.douban.com/subject/30133440/):这里一本国人编写的系统入门 Java 的书籍,深入浅出,内容全面; ++ 《Java 核心技术》:目前最新的是第 10 版,有[卷一](https://book.douban.com/subject/26880667/) 和[卷二](https://book.douban.com/subject/27165931/) 两册,卷二可以选择性阅读,因为其中很多章节的内容在实际开发中很少用到。 + +目前大多数框架要求 Java 版本至少是 1.8,这是由于 Java 1.8 提供了函数式编程,使得可以用更精简的代码来实现之前同样的功能,比如你调用 Spark API,使用 1.8 可能比 1.7 少数倍的代码,所以这里额外推荐阅读 [《Java 8 实战》](https://book.douban.com/subject/26772632/) 这本书籍。 + +#### 2. Scala + +Scala 是一门综合了面向对象和函数式编程概念的静态类型的编程语言,它运行在 Java 虚拟机上,可以与所有的 Java 类库无缝协作,著名的 Kafka 就是采用 Scala 语言进行开发的。 + +为什么需要学习 Scala 语言 ? 这是因为当前最火的计算框架 Flink 和 Spark 都提供了 Scala 语言的接口,使用它进行开发,比使用 Java 8 所需要的代码更少,且 Spark 就是使用 Scala 语言进行编写的,学习 Scala 可以帮助你更深入的理解 Spark。同样的,对于习惯书本学习的小伙伴,这里推荐两本入门书籍: + +- [《快学 Scala(第 2 版)》](https://book.douban.com/subject/27093751/) +- [《Scala 编程 (第 3 版)》](https://book.douban.com/subject/27591387/) + +> 这里说明一下,如果你的时间有限,不一定要学完 Scala 才去学习大数据框架。Scala 确实足够的精简和灵活,但其在语言复杂度上略大于 Java,例如隐式转换和隐式参数等概念在初次涉及时会比较难以理解,所以你可以在了解 Spark 后再去学习 Scala,因为类似隐式转换等概念在 Spark 源码中有大量的运用。 + +### 2.2 Linux 基础 + +通常大数据框架都部署在 Linux 服务器上,所以需要具备一定的 Linux 知识。Linux 书籍当中比较著名的是 《鸟哥私房菜》系列,这个系列很全面也很经典。但如果你希望能够快速地入门,这里推荐[《Linux 就该这么学》](https://www.linuxprobe.com/),其网站上有免费的电子书版本。 + +### 2.3 构建工具 + +这里需要掌握的自动化构建工具主要是 Maven。Maven 在大数据场景中使用比较普遍,主要在以下三个方面: + ++ 管理项目 JAR 包,帮助你快速构建大数据应用程序; ++ 不论你的项目是使用 Java 语言还是 Scala 语言进行开发,提交到集群环境运行时,都需要使用 Maven 进行编译打包; ++ 大部分大数据框架使用 Maven 进行源码管理,当你需要从其源码编译出安装包时,就需要使用到 Maven。 + +### 2.4 框架学习 + +#### 1. 框架分类 + +上面我们介绍了很多大数据框架,这里进行一下分类总结: + +**日志收集框架**:Flume 、Logstash、Kibana + +**分布式文件存储系统**:Hadoop HDFS + +**数据库系统**:Mongodb、HBase + +**分布式计算框架**: + ++ 批处理框架:Hadoop MapReduce ++ 流处理框架:Storm ++ 混合处理框架:Spark、Flink + +**查询分析框架**:Hive 、Spark SQL 、Flink SQL、 Pig、Phoenix + +**集群资源管理器**:Hadoop YARN + +**分布式协调服务**:Zookeeper + +**数据迁移工具**:Sqoop + +**任务调度框架**:Azkaban、Oozie + +**集群部署和监控**:Ambari、Cloudera Manager + +上面列出的都是比较主流的大数据框架,社区都很活跃,学习资源也比较丰富。建议从 Hadoop 开始入门学习,因为它是整个大数据生态圈的基石,其它框架都直接或者间接依赖于 Hadoop 。接着就可以学习计算框架,Spark 和 Flink 都是比较主流的混合处理框架,Spark 出现得较早,所以其应用也比较广泛。 Flink 是当下最火热的新一代的混合处理框架,其凭借众多优异的特性得到了众多公司的青睐。两者可以按照你个人喜好或者实际工作需要进行学习。 + +
+ +> *图片引用自* :*https://www.edureka.co/blog/hadoop-ecosystem* + +至于其它框架,在学习上并没有特定的先后顺序,如果你的学习时间有限,建议初次学习时候,同一类型的框架掌握一种即可,比如日志收集框架就有很多种,初次学习时候只需要掌握一种,能够完成日志收集的任务即可,之后工作上有需要可以再进行针对性地学习。 + +#### 2. 学习资料 + +大数据最权威和最全面的学习资料就是官方文档。热门的大数据框架社区都比较活跃、版本更新迭代也比较快,所以其出版物都明显滞后于其实际版本,基于这个原因采用书本学习不是一个最好的方案。比较庆幸的是,大数据框架的官方文档都写的比较好,内容完善,重点突出,同时都采用了大量配图进行辅助讲解。当然也有一些优秀的书籍历经时间的检验,至今依然很经典,这里列出部分个人阅读过的经典书籍: + +- [《hadoop 权威指南 (第四版)》](https://book.douban.com/subject/27115351/) 2017 年 +- [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) 2017 年 +- [《从 Paxos 到 Zookeeper 分布式一致性原理与实践》](https://book.douban.com/subject/26292004/) 2015 年 +- [《Spark 技术内幕 深入解析 Spark 内核架构设计与实现原理》](https://book.douban.com/subject/26649141/) 2015 年 +- [《Spark.The.Definitive.Guide》](https://book.douban.com/subject/27035127/) 2018 年 +- [《HBase 权威指南》](https://book.douban.com/subject/10748460/) 2012 年 +- [《Hive 编程指南》](https://book.douban.com/subject/25791255/) 2013 年 + +#### 3. 视频学习资料 + +上面我推荐的都是书籍学习资料,很少推荐视频学习资料,这里说明一下原因:因为书籍历经时间的考验,能够再版的或者豆瓣等平台评价高的证明都是被大众所认可的,从概率的角度上来说,其必然更加优秀,不容易浪费大家的学习时间和精力,所以我个人更倾向于官方文档或者书本的学习方式,而不是视频。因为视频学习资料,缺少一个公共的评价平台和完善的评价机制,所以其质量良莠不齐。但是视频任然有其不可替代的好处,学习起来更直观、印象也更深刻,所以对于习惯视频学习的小伙伴,这里我各推荐一个免费的和付费的视频学习资源,大家按需选择: + ++ 免费学习资源:尚硅谷大数据学习路线 —— [下载链接](http://www.atguigu.com/bigdata_video.shtml#bigdata) \ [在线观看链接](https://space.bilibili.com/302417610/) ++ 付费学习资源:[慕课网 Michael PK 的系列课程](https://www.imooc.com/t/2781843) + +## 三、开发工具 + +这里推荐一些大数据常用的开发工具: + +**Java IDE**:IDEA 和 Eclipse 都可以。从个人使用习惯而言,更倾向于 IDEA ; + +**VirtualBox**:在学习过程中,你可能经常要在虚拟机上搭建服务和集群。VirtualBox 是一款开源、免费的虚拟机管理软件,虽然是轻量级软件,但功能很丰富,基本能够满足日常的使用需求; + +**MobaXterm**:大数据的框架通常都部署在服务器上,这里推荐使用 MobaXterm 进行连接。同样是免费开源的,支持多种连接协议,支持拖拽上传文件,支持使用插件扩展; + +**Translate Man**:一款浏览器上免费的翻译插件 (谷歌和火狐均支持)。它采用谷歌的翻译接口,准确性非常高,支持划词翻译,可以辅助进行官方文档的阅读。 + +## 四、结语 + +以上就是个人关于大数据的学习心得和路线推荐。本片文章对大数据技术栈做了比较狭义的限定,随着学习的深入,大家也可以把 Python 语言、推荐系统、机器学习等逐步加入到自己的大数据技术栈中。 + diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\270\270\347\224\250\350\275\257\344\273\266\345\256\211\350\243\205\346\214\207\345\215\227.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\270\270\347\224\250\350\275\257\344\273\266\345\256\211\350\243\205\346\214\207\345\215\227.md" new file mode 100644 index 0000000..390afb8 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\270\270\347\224\250\350\275\257\344\273\266\345\256\211\350\243\205\346\214\207\345\215\227.md" @@ -0,0 +1,67 @@ +## 大数据常用软件安装指南 + +为方便大家查阅,本仓库所有软件的安装方式单独整理如下: + +### 一、基础软件安装 + +1. [Linux 环境下 JDK 安装](installation/Linux下JDK安装.md) +2. [Linux 环境下 Python 安装](installation/Linux下Python安装.md) +3. [虚拟机静态 IP 及多 IP 配置](installation/虚拟机静态IP及多IP配置.md) + +### 二、Hadoop + +1. [Hadoop 单机环境搭建](installation/Hadoop单机环境搭建.md) +2. [Hadoop 集群环境搭建](installation/Hadoop集群环境搭建.md) +3. [基于 Zookeeper 搭建 Hadoop 高可用集群](installation/基于Zookeeper搭建Hadoop高可用集群.md) + +### 三、Spark + +1. [Spark 开发环境搭建](installation/Spark开发环境搭建.md) +2. [基于 Zookeeper 搭建 Spark 高可用集群](installation/Spark集群环境搭建.md) + +### 四、Flink + +1. [Flink Standalone 集群部署](installation/Flink_Standalone_Cluster.md) + +### 五、Storm + +1. [Storm 单机环境搭建](installation/Storm单机环境搭建.md) +2. [Storm 集群环境搭建](installation/Storm集群环境搭建.md) + +### 六、HBase + +1. [HBase 单机环境搭建](installation/HBase单机环境搭建.md) +2. [HBase 集群环境搭建](installation/HBase集群环境搭建.md) + +### 七、Flume + +1. [Linux 环境下 Flume 的安装部署](installation/Linux下Flume的安装.md) + +### 八、Azkaban + +1. [Azkaban3.x 编译及部署](installation/Azkaban_3.x_编译及部署.md) + +### 九、Hive + +1. [Linux 环境下 Hive 的安装部署](installation/Linux环境下Hive的安装部署.md) + +### 十、Zookeeper + +1. [Zookeeper 单机环境和集群环境搭建](installation/Zookeeper单机环境和集群环境搭建.md) + +### 十一、Kafka + +1. [基于 Zookeeper 搭建 Kafka 高可用集群](installation/基于Zookeeper搭建Kafka高可用集群.md) + + +### 版本说明 + +由于 Apache Hadoop 原有安装包之间兼容性比较差,所以如无特殊需求,本仓库一律选择 **CDH** (Cloudera's Distribution, including Apache Hadoop) 版本的安装包。它基于稳定版本的 Apache Hadoop 构建,并做了兼容性测试,是目前生产环境中使用最为广泛的版本。 + +最新的 CDH 5 的下载地址为:http://archive.cloudera.com/cdh5/cdh/5/ 。这个页面很大且加载速度比较慢,需要耐心等待页面加载完成。上半部分是文档链接,后半部分才是安装包。同一个 CDH 版本的不同框架间都做了集成测试,可以保证没有任何 JAR 包冲突。安装包包名通常如下所示,这里 CDH 版本都是 `5.15.2` ,前面是各个软件自己的版本 ,未避免出现不必要的 JAR 包冲突,**请务必保持 CDH 的版本一致**。 + +```hsell +hadoop-2.6.0-cdh5.15.2.tar.gz +hbase-1.2.0-cdh5.15.2 +hive-1.1.0-cdh5.15.2.tar.gz +``` diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\272\224\347\224\250\345\270\270\347\224\250\346\211\223\345\214\205\346\226\271\345\274\217.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\272\224\347\224\250\345\270\270\347\224\250\346\211\223\345\214\205\346\226\271\345\274\217.md" new file mode 100644 index 0000000..84d7b7e --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\345\272\224\347\224\250\345\270\270\347\224\250\346\211\223\345\214\205\346\226\271\345\274\217.md" @@ -0,0 +1,306 @@ +# 大数据应用常用打包方式 + + + + + + + +## 一、简介 + +在提交大数据作业到集群上运行时,通常需要先将项目打成 JAR 包。这里以 Maven 为例,常用打包方式如下: + +- 不加任何插件,直接使用 mvn package 打包; +- 使用 maven-assembly-plugin 插件; +- 使用 maven-shade-plugin 插件; +- 使用 maven-jar-plugin 和 maven-dependency-plugin 插件; + +以下分别进行详细的说明。 + +## 二、mvn package + +不在 POM 中配置任何插件,直接使用 `mvn package` 进行项目打包,这对于没有使用外部依赖包的项目是可行的。但如果项目中使用了第三方 JAR 包,就会出现问题,因为 `mvn package` 打的 JAR 包中是不含有依赖包,会导致作业运行时出现找不到第三方依赖的异常。这种方式局限性比较大,因为实际的项目往往很复杂,通常都会依赖第三方 JAR。 + +大数据框架的开发者也考虑到这个问题,所以基本所有的框架都支持在提交作业时使用 `--jars` 指定第三方依赖包,但是这种方式的问题同样很明显,就是你必须保持生产环境与开发环境中的所有 JAR 包版本一致,这是有维护成本的。 + +基于上面这些原因,最简单的是采用 `All In One` 的打包方式,把所有依赖都打包到一个 JAR 文件中,此时对环境的依赖性最小。要实现这个目的,可以使用 Maven 提供的 `maven-assembly-plugin` 或 `maven-shade-plugin` 插件。 + +## 三、maven-assembly-plugin插件 + +`Assembly` 插件支持将项目的所有依赖、文件都打包到同一个输出文件中。目前支持输出以下文件类型: + +- zip +- tar +- tar.gz (or tgz) +- tar.bz2 (or tbz2) +- tar.snappy +- tar.xz (or txz) +- jar +- dir +- war + +### 3.1 基本使用 +在 POM.xml 中引入插件,指定打包格式的配置文件 `assembly.xml`(名称可自定义),并指定作业的主入口类: + +```xml + + + + maven-assembly-plugin + + + src/main/resources/assembly.xml + + + + com.heibaiying.wordcount.ClusterWordCountApp + + + + + + +``` + +assembly.xml 文件内容如下: + +```xml + + + jar-with-dependencies + + + + jar + + + false + + + / + true + true + runtime + + + org.apache.storm:storm-core + + + + +``` + +### 3.2 打包命令 + +采用 maven-assembly-plugin 进行打包时命令如下: + +```shell +# mvn assembly:assembly +``` + +打包后会同时生成两个 JAR 包,其中后缀为 `jar-with-dependencies` 是含有第三方依赖的 JAR 包,后缀是由 `assembly.xml` 中 `` 标签指定的,可以自定义修改。 + +
+ + + + + +## 四、maven-shade-plugin插件 + +`maven-shade-plugin` 比 `maven-assembly-plugin` 功能更为强大,比如你的工程依赖很多的 JAR 包,而被依赖的 JAR 又会依赖其他的 JAR 包,这样,当工程中依赖到不同的版本的 JAR 时,并且 JAR 中具有相同名称的资源文件时,shade 插件会尝试将所有资源文件打包在一起时,而不是和 assembly 一样执行覆盖操作。 + +**通常使用 `maven-shade-plugin` 就能够完成大多数的打包需求,其配置简单且适用性最广,因此建议优先使用此方式。** + +### 4.1 基本配置 + +采用 `maven-shade-plugin` 进行打包时候,配置示例如下: + +```xml + + org.apache.maven.plugins + maven-shade-plugin + + true + + + *:* + + META-INF/*.SF + META-INF/*.sf + META-INF/*.DSA + META-INF/*.dsa + META-INF/*.RSA + META-INF/*.rsa + META-INF/*.EC + META-INF/*.ec + META-INF/MSFTSIG.SF + META-INF/MSFTSIG.RSA + + + + + + org.apache.storm:storm-core + + + + + + package + + shade + + + + + + + + + + + +``` + +以上配置来源于 Storm Github,在上面的配置中,排除了部分文件,这是因为有些 JAR 包生成时,会使用 jarsigner 生成文件签名 (完成性校验),分为两个文件存放在 META-INF 目录下: + +- a signature file, with a .SF extension; +- a signature block file, with a .DSA, .RSA, or .EC extension。 + +如果某些包的存在重复引用,这可能会导致在打包时候出现 `Invalid signature file digest for Manifest main attributes` 异常,所以在配置中排除这些文件。 + +### 4.2 打包命令 + +使用 maven-shade-plugin 进行打包的时候,打包命令和普通打包一样: + +```shell +# mvn package +``` + +打包后会生成两个 JAR 包,提交到服务器集群时使用非 original 开头的 JAR。 + +
+ + + +## 五、其他打包需求 + +### 1. 使用非Maven仓库中的Jar + +通常上面两种打包能够满足大多数的使用场景。但是如果你想把某些没有被 Maven 管理 JAR 包打入到最终的 JAR 中,比如你在 `resources/lib` 下引入的其他非 Maven 仓库中的 JAR,此时可以使用 `maven-jar-plugin` 和 `maven-dependency-plugin` 插件将其打入最终的 JAR 中。 + +```xml + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + + lib/ + + com.heibaiying.BigDataApp + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + compile + + + copy-dependencies + + + + + ${project.build.directory}/lib + + + + + + + +``` + +### 2. 排除集群中已经存在的Jar + +通常为了避免冲突,官方文档都会建议你排除集群中已经提供的 JAR 包,如下: + +Spark 官方文档 Submitting Applications 章节: + +> When creating assembly jars, list Spark and Hadoop as `provided` dependencies; these need not be bundled since they are provided by the cluster manager at runtime. + +Strom 官方文档 Running Topologies on a Production Cluster 章节: + +>Then run mvn assembly:assembly to get an appropriately packaged jar. Make sure you exclude the Storm jars since the cluster already has Storm on the classpath. + +按照以上说明,排除 JAR 包的方式主要有两种: + ++ 对需要排除的依赖添加 `provided` 标签,此时该 JAR 包会被排除,但是不建议使用这种方式,因为此时你在本地运行也无法使用该 JAR 包; ++ 建议直接在 `maven-assembly-plugin` 或 `maven-shade-plugin` 的配置文件中使用 `` 进行排除。 + +### 3. 打包Scala文件 + +如果你使用到 Scala 语言进行编程,此时需要特别注意 :默认情况下 Maven 是不会把 `scala` 文件打入最终的 JAR 中,需要额外添加 `maven-scala-plugin` 插件,常用配置如下: + +```xml + + org.scala-tools + maven-scala-plugin + 2.15.1 + + + scala-compile + + compile + + + + **/*.scala + + + + + scala-test-compile + + testCompile + + + + +``` + + + +## 参考资料 +关于 Maven 各个插件的详细配置可以查看其官方文档: ++ maven-assembly-plugin : http://maven.apache.org/plugins/maven-assembly-plugin/ ++ maven-shade-plugin : http://maven.apache.org/plugins/maven-shade-plugin/ ++ maven-jar-plugin : http://maven.apache.org/plugins/maven-jar-plugin/ ++ maven-dependency-plugin : http://maven.apache.org/components/plugins/maven-dependency-plugin/ + +关于 maven-shade-plugin 的更多配置也可以参考该博客: [maven-shade-plugin 入门指南](https://www.jianshu.com/p/7a0e20b30401) diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\346\212\200\346\234\257\346\240\210\346\200\235\347\273\264\345\257\274\345\233\276.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\346\212\200\346\234\257\346\240\210\346\200\235\347\273\264\345\257\274\345\233\276.md" new file mode 100644 index 0000000..89c8648 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\345\244\247\346\225\260\346\215\256\346\212\200\346\234\257\346\240\210\346\200\235\347\273\264\345\257\274\345\233\276.md" @@ -0,0 +1,2 @@ +
+ diff --git "a/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\350\265\204\346\226\231\345\210\206\344\272\253\344\270\216\345\267\245\345\205\267\346\216\250\350\215\220.md" "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\350\265\204\346\226\231\345\210\206\344\272\253\344\270\216\345\267\245\345\205\267\346\216\250\350\215\220.md" new file mode 100644 index 0000000..b7e3b33 --- /dev/null +++ "b/\345\244\247\346\225\260\346\215\256\346\241\206\346\236\266\345\255\246\344\271\240/\350\265\204\346\226\231\345\210\206\344\272\253\344\270\216\345\267\245\345\205\267\346\216\250\350\215\220.md" @@ -0,0 +1,56 @@ +这里分享一些自己学习过程中觉得不错的资料和开发工具。 + + + +## :book: 经典书籍 + +- [《hadoop 权威指南 (第四版)》](https://book.douban.com/subject/27115351/) 2017 年 +- [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) 2017 年 +- [《从 Paxos 到 Zookeeper 分布式一致性原理与实践》](https://book.douban.com/subject/26292004/) 2015 年 +- [《Spark 技术内幕 深入解析 Spark 内核架构设计与实现原理》](https://book.douban.com/subject/26649141/) 2015 年 +- [《Spark.The.Definitive.Guide》](https://book.douban.com/subject/27035127/) 2018 年 +- [《HBase 权威指南》](https://book.douban.com/subject/10748460/) 2012 年 +- [《Hive 编程指南》](https://book.douban.com/subject/25791255/) 2013 年 +- [《快学 Scala(第 2 版)》](https://book.douban.com/subject/27093751/) 2017 年 +- [《Scala 编程》](https://book.douban.com/subject/27591387/) 2018 年 + + + +## :computer: 官方文档 + +上面的书籍我都列出了出版日期,可以看到大部分书籍的出版时间都比较久远了,虽然这些书籍比较经典,但是很多书籍在软件版本上已经滞后了很多。所以推荐优先选择各个框架的**官方文档**作为学习资料。大数据框架的官方文档都很全面,并且对知识点的讲解都做到了简明扼要。这里以 [Spark RDD 官方文档](https://spark.apache.org/docs/latest/rdd-programming-guide.html) 为例,你会发现不仅清晰的知识点导航,而且所有示例都给出了 Java,Scala,Python 三种语言的版本,除了官方文档,其他书籍很少能够做到这一点。 + + + +## :orange_book: 优秀博客 + +- 有态度的 HBase/Spark/BigData:http://hbasefly.com/ +- 深入 Apache Spark 的设计和实现原理 : https://github.com/JerryLead/SparkInternals +- Jark's Blog - Flink 系列文章:http://wuchong.me/categories/Flink/ + + + +## :triangular_ruler:开发工具 + +#### 1. VirtualBox + +一款开源、免费的虚拟机管理软件,虽然是轻量级软件,但功能很丰富,基本能够满足全部的使用需求。 + +官方网站:https://www.virtualbox.org/ + +#### 2. MobaXterm + +大数据的框架通常都部署在服务器上,这里推荐使用 MobaXterm 进行连接。同样是免费开源的,支持多种连接协议,支持拖拽上传文件,支持使用插件扩展。 + +官方网站:https://mobaxterm.mobatek.net/ + +#### 3. Translate Man + +Translate Man 是一款浏览器上的翻译插件 (谷歌和火狐均支持)。它采用谷歌的翻译接口,准确性非常高,支持划词翻译,可以辅助进行官方文档的阅读。 + +#### 4. ProcessOn + +ProcessOn 式一个在线绘图平台,使用起来非常便捷,可以用于笔记或者博客配图的绘制。 + +官方网站:https://www.processon.com/ +