நடைமுறையில் ஸ்கார்க் ஸ்கீமாஎவல்யூஷன்

அன்புள்ள வாசகர்களே, நல்ல நாள்!

இந்தக் கட்டுரையில், நியோஃப்ளெக்ஸின் பிக் டேட்டா சொல்யூஷன்ஸ் வணிகப் பகுதியின் முன்னணி ஆலோசகர், அப்பாச்சி ஸ்பார்க்கைப் பயன்படுத்தி மாறி கட்டமைப்பு ஷோகேஸ்களை உருவாக்குவதற்கான விருப்பங்களை விரிவாக விவரிக்கிறார்.

தரவு பகுப்பாய்வு திட்டத்தின் ஒரு பகுதியாக, தளர்வான கட்டமைக்கப்பட்ட தரவுகளின் அடிப்படையில் ஸ்டோர்ஃப்ரன்ட்களை உருவாக்கும் பணி அடிக்கடி எழுகிறது.

பொதுவாக இவை பதிவுகள் அல்லது பல்வேறு அமைப்புகளின் பதில்கள், JSON அல்லது XML ஆக சேமிக்கப்படும். தரவு ஹடூப்பில் பதிவேற்றப்பட்டது, பின்னர் நீங்கள் அவர்களிடமிருந்து ஒரு ஸ்டோர்ஃபிரண்டை உருவாக்க வேண்டும். உருவாக்கப்பட்ட காட்சி பெட்டிக்கான அணுகலை நாம் ஏற்பாடு செய்யலாம், எடுத்துக்காட்டாக, இம்பாலா மூலம்.

இந்த வழக்கில், இலக்கு கடை முகப்பின் திட்டம் முன்பே தெரியவில்லை. மேலும், இந்தத் திட்டத்தை முன்கூட்டியே வரைய முடியாது, ஏனெனில் இது தரவைப் பொறுத்தது, மேலும் இந்த மிகவும் தளர்வான கட்டமைக்கப்பட்ட தரவை நாங்கள் கையாள்கிறோம்.

எடுத்துக்காட்டாக, இன்று பின்வரும் பதில் பதிவு செய்யப்பட்டுள்ளது:

{source: "app1", error_code: ""}

நாளை அதே அமைப்பிலிருந்து பின்வரும் பதில் வரும்:

{source: "app1", error_code: "error", description: "Network error"}

இதன் விளைவாக, ஷோகேஸில் மேலும் ஒரு புலம் சேர்க்கப்பட வேண்டும் - விளக்கம், அது வருமா இல்லையா என்பது யாருக்கும் தெரியாது.

அத்தகைய தரவுகளில் ஒரு ஸ்டோர்ஃபிரண்டை உருவாக்கும் பணி மிகவும் நிலையானது, மேலும் ஸ்பார்க் இதற்கு பல கருவிகளைக் கொண்டுள்ளது. மூலத் தரவைப் பாகுபடுத்துவதற்கு, JSON மற்றும் XML ஆகிய இரண்டிற்கும் ஆதரவு உள்ளது, மேலும் முன்னர் அறியப்படாத ஸ்கீமாவிற்கு, schemaEvolutionக்கான ஆதரவு வழங்கப்படுகிறது.

முதல் பார்வையில், தீர்வு எளிமையானது. நீங்கள் JSON உடன் ஒரு கோப்புறையை எடுத்து டேட்டாஃப்ரேமில் படிக்க வேண்டும். ஸ்பார்க் ஒரு திட்டத்தை உருவாக்கி, உள்ளமைக்கப்பட்ட தரவை கட்டமைப்புகளாக மாற்றும். மேலும், ஹைவ் மெட்டாஸ்டோரில் கடை முகப்பைப் பதிவு செய்வதன் மூலம், இம்பாலாவிலும் ஆதரிக்கப்படும் பார்க்வெட்டில் அனைத்தையும் சேமிக்க வேண்டும்.

எல்லாம் எளிமையானது போல் தெரிகிறது.

இருப்பினும், நடைமுறையில் உள்ள பல சிக்கல்களை என்ன செய்வது என்பது ஆவணத்தில் உள்ள சிறிய எடுத்துக்காட்டுகளிலிருந்து தெளிவாக இல்லை.

ஸ்டோர்ஃப்ரண்டை உருவாக்காமல், JSON அல்லது XMLஐ டேட்டாஃப்ரேமில் படிப்பதற்கான அணுகுமுறையை ஆவணமாக்கல் விவரிக்கிறது.

அதாவது, இது JSON ஐ எவ்வாறு படிப்பது மற்றும் அலசுவது என்பதைக் காட்டுகிறது:

df = spark.read.json(path...)

ஸ்பார்க்கிற்கு தரவு கிடைக்க இது போதுமானது.

நடைமுறையில், ஒரு கோப்புறையிலிருந்து JSON கோப்புகளைப் படித்து ஒரு டேட்டாஃப்ரேமை உருவாக்குவதை விட ஸ்கிரிப்ட் மிகவும் சிக்கலானது. நிலைமை இதுபோல் தெரிகிறது: ஏற்கனவே ஒரு குறிப்பிட்ட கடை முகப்பு உள்ளது, ஒவ்வொரு நாளும் புதிய தரவு வருகிறது, அவை ஸ்டோர்ஃபிரண்டில் சேர்க்கப்பட வேண்டும், திட்டம் வேறுபடலாம் என்பதை மறந்துவிடாதீர்கள்.

ஒரு காட்சி பெட்டியை உருவாக்குவதற்கான வழக்கமான திட்டம் பின்வருமாறு:

1 படி. தினசரி மறுஏற்றத்துடன் தரவு ஹடூப்பில் ஏற்றப்பட்டு புதிய பகிர்வில் சேர்க்கப்படுகிறது. இது நாள் வாரியாக பிரிக்கப்பட்ட ஆரம்ப தரவுகளுடன் ஒரு கோப்புறையாக மாறும்.

2 படி. ஆரம்ப சுமையின் போது, ​​இந்த கோப்புறை ஸ்பார்க்கால் படிக்கப்பட்டு பாகுபடுத்தப்படுகிறது. இதன் விளைவாக வரும் டேட்டாஃப்ரேம் பாகுபடுத்தக்கூடிய வடிவத்தில் சேமிக்கப்படுகிறது, எடுத்துக்காட்டாக, பார்க்வெட்டில், பின்னர் அதை இம்பாலாவில் இறக்குமதி செய்யலாம். இது வரை திரட்டப்பட்ட அனைத்து தரவுகளுடன் ஒரு இலக்கு காட்சி பெட்டியை உருவாக்குகிறது.

3 படி. ஒவ்வொரு நாளும் கடையின் முகப்புப் பகுதியைப் புதுப்பிக்கும் ஒரு பதிவிறக்கம் உருவாக்கப்பட்டது.
அதிகரிக்கும் ஏற்றுதல், ஷோகேஸைப் பிரிக்க வேண்டிய அவசியம் மற்றும் ஷோகேஸின் பொதுவான திட்டத்தைப் பராமரிப்பது பற்றிய கேள்வி உள்ளது.

ஒரு உதாரணத்தை எடுத்துக் கொள்வோம். ஒரு களஞ்சியத்தை உருவாக்குவதற்கான முதல் படி செயல்படுத்தப்பட்டது என்று வைத்துக்கொள்வோம், மேலும் JSON கோப்புகள் ஒரு கோப்புறையில் பதிவேற்றப்படுகின்றன.

அவற்றிலிருந்து டேட்டாஃப்ரேமை உருவாக்கி, அதை காட்சிப் பொருளாகச் சேமிப்பது ஒரு பிரச்சனையல்ல. ஸ்பார்க் ஆவணத்தில் எளிதாகக் காணக்கூடிய முதல் படி இது:

df = spark.read.option("mergeSchema", True).json(".../*") 
df.printSchema()

root 
|-- a: long (nullable = true) 
|-- b: string (nullable = true) 
|-- c: struct (nullable = true) |    
|-- d: long (nullable = true)

எல்லாம் நன்றாக இருப்பதாகத் தெரிகிறது.

நாங்கள் JSON ஐப் படித்து பாகுபடுத்தினோம், பின்னர் டேட்டாஃப்ரேமை ஒரு பார்க்வெட்டாகச் சேமித்து, ஹைவில் எந்த வசதியான வழியிலும் பதிவு செய்கிறோம்:

df.write.format(“parquet”).option('path','<External Table Path>').saveAsTable('<Table Name>')

எங்களுக்கு ஒரு சாளரம் கிடைக்கிறது.

ஆனால், அடுத்த நாள், மூலத்திலிருந்து புதிய தரவு சேர்க்கப்பட்டது. எங்களிடம் JSON உடன் ஒரு கோப்புறை உள்ளது, மேலும் இந்த கோப்புறையிலிருந்து ஒரு காட்சி பெட்டி உருவாக்கப்பட்டுள்ளது. மூலத்திலிருந்து அடுத்த தொகுதி தரவை ஏற்றிய பிறகு, டேட்டா மார்ட்டில் ஒரு நாள் மதிப்புள்ள தரவு இல்லை.

தர்க்கரீதியான தீர்வு, கடையின் முகப்பை நாளுக்கு நாள் பிரிப்பதாகும், இது ஒவ்வொரு அடுத்த நாளும் ஒரு புதிய பகிர்வைச் சேர்க்க அனுமதிக்கும். இதற்கான பொறிமுறையும் நன்கு அறியப்பட்டதாகும், ஸ்பார்க் நீங்கள் தனித்தனியாக பகிர்வுகளை எழுத அனுமதிக்கிறது.

முதலில், நாங்கள் ஒரு ஆரம்ப சுமையைச் செய்கிறோம், மேலே விவரிக்கப்பட்டுள்ளபடி தரவைச் சேமித்து, பகிர்வை மட்டும் சேர்க்கிறோம். இந்தச் செயல் ஸ்டோர்ஃபிரண்ட் துவக்கம் என்று அழைக்கப்படுகிறது மற்றும் ஒருமுறை மட்டுமே செய்யப்படுகிறது:

df.write.partitionBy("date_load").mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable)

அடுத்த நாள், ஒரு புதிய பகிர்வை மட்டும் ஏற்றுவோம்:

df.coalesce(1).write.mode("overwrite").parquet(dbpath + "/" + db + "/" + destTable +"/date_load=" + date_load + "/")

ஸ்கீமாவைப் புதுப்பிக்க ஹைவில் மீண்டும் பதிவு செய்வது மட்டுமே எஞ்சியுள்ளது.
இருப்பினும், இங்குதான் பிரச்சினைகள் எழுகின்றன.

முதல் பிரச்சனை. விரைவில் அல்லது பின்னர், இதன் விளைவாக வரும் parquet படிக்க முடியாததாக இருக்கும். parquet மற்றும் JSON வெற்றுப் புலங்களை வித்தியாசமாக நடத்துவதே இதற்குக் காரணம்.

ஒரு பொதுவான சூழ்நிலையை கருத்தில் கொள்வோம். உதாரணமாக, நேற்று JSON வந்தது:

День 1: {"a": {"b": 1}},

இன்று அதே JSON இப்படி இருக்கிறது:

День 2: {"a": null}

எங்களிடம் இரண்டு வெவ்வேறு பகிர்வுகள் உள்ளன, ஒவ்வொன்றும் ஒரு வரியுடன்.
முழு மூலத் தரவையும் நாம் படிக்கும்போது, ​​ஸ்பார்க் வகையைத் தீர்மானிக்க முடியும், மேலும் "a" என்பது INT வகை "b" உள்ளமைக்கப்பட்ட புலத்துடன் "கட்டமைப்பு" வகையின் புலம் என்பதை புரிந்து கொள்ளும். ஆனால், ஒவ்வொரு பகிர்வும் தனித்தனியாக சேமிக்கப்பட்டிருந்தால், பொருந்தாத பகிர்வு திட்டங்களுடன் ஒரு அழகு வேலைப்பாடு கிடைக்கும்:

df1 (a: <struct<"b": INT>>)
df2 (a: STRING NULLABLE)

இந்த நிலைமை நன்கு அறியப்பட்டதாகும், எனவே ஒரு விருப்பம் சிறப்பாகச் சேர்க்கப்பட்டுள்ளது - மூலத் தரவைப் பாகுபடுத்தும் போது, ​​காலியான புலங்களை அகற்றவும்:

df = spark.read.json("...", dropFieldIfAllNull=True)

இந்த வழக்கில், பார்க்வெட் ஒன்றாக படிக்கக்கூடிய பகிர்வுகளைக் கொண்டிருக்கும்.
என்றாலும் நடைமுறையில் இதைச் செய்தவர்கள் இங்கே கசப்பாகச் சிரிப்பார்கள். ஏன்? ஆம், ஏனென்றால் இன்னும் இரண்டு சூழ்நிலைகள் இருக்கலாம். அல்லது மூன்று. அல்லது நான்கு. முதலாவது, கிட்டத்தட்ட நிச்சயமாக நிகழும், வெவ்வேறு JSON கோப்புகளில் எண் வகைகள் வித்தியாசமாக இருக்கும். எடுத்துக்காட்டாக, {intField: 1} மற்றும் {intField: 1.1}. அத்தகைய புலங்கள் ஒரு பகிர்வில் காணப்பட்டால், ஸ்கீமா ஒன்றிணைப்பு எல்லாவற்றையும் சரியாகப் படிக்கும், இது மிகவும் துல்லியமான வகைக்கு வழிவகுக்கும். ஆனால் வெவ்வேறுவற்றில் இருந்தால், ஒன்று intField: int, மற்றொன்று intField: double இருக்கும்.

இந்த சூழ்நிலையை கையாள பின்வரும் கொடி உள்ளது:

df = spark.read.json("...", dropFieldIfAllNull=True, primitivesAsString=True)

இப்போது எங்களிடம் ஒரு கோப்புறை உள்ளது, அங்கு பகிர்வுகள் ஒரு டேட்டாஃப்ரேமிலும் முழு ஷோகேஸின் சரியான பார்க்வெட்டிலும் படிக்க முடியும். ஆம்? இல்லை.

நாங்கள் ஹைவில் அட்டவணையை பதிவு செய்தோம் என்பதை நினைவில் கொள்ள வேண்டும். புல பெயர்களில் ஹைவ் கேஸ் சென்சிடிவ் அல்ல, அதே சமயம் பார்கெட் என்பது கேஸ் சென்சிட்டிவ். எனவே, ஸ்கீமாக்கள் கொண்ட பகிர்வுகள்: field1: int, மற்றும் Field1: int ஆகியவை ஹைவ்க்கு ஒரே மாதிரியானவை, ஆனால் ஸ்பார்க்கிற்கு அல்ல. புலத்தின் பெயர்களை சிறிய எழுத்துக்களுக்கு மாற்ற மறக்காதீர்கள்.

அதன் பிறகு, எல்லாம் சரியாகிவிட்டது போல் தெரிகிறது.

இருப்பினும், எல்லாம் அவ்வளவு எளிதல்ல. இரண்டாவது, நன்கு அறியப்பட்ட சிக்கல் உள்ளது. ஒவ்வொரு புதிய பகிர்வும் தனித்தனியாக சேமிக்கப்படுவதால், பகிர்வு கோப்புறையில் Spark சேவை கோப்புகள் இருக்கும், எடுத்துக்காட்டாக, _SUCCESS செயல்பாட்டு வெற்றிக் கொடி. பார்கெட் செய்ய முயற்சிக்கும்போது இது பிழையை ஏற்படுத்தும். இதைத் தவிர்க்க, ஸ்பார்க் கோப்புறையில் சேவைக் கோப்புகளைச் சேர்ப்பதைத் தடுக்க நீங்கள் உள்ளமைவை உள்ளமைக்க வேண்டும்:

hadoopConf = sc._jsc.hadoopConfiguration()
hadoopConf.set("parquet.enable.summary-metadata", "false")
hadoopConf.set("mapreduce.fileoutputcommitter.marksuccessfuljobs", "false")

இப்போது ஒவ்வொரு நாளும் ஒரு புதிய பார்க்வெட் பகிர்வு இலக்கு ஷோகேஸ் கோப்புறையில் சேர்க்கப்படுகிறது, அங்கு அன்றைய பாகுபடுத்தப்பட்ட தரவு அமைந்துள்ளது. தரவு வகை முரண்பாட்டுடன் பகிர்வுகள் எதுவும் இல்லை என்பதை நாங்கள் முன்கூட்டியே கவனித்துக்கொண்டோம்.

ஆனால், நமக்கு மூன்றாவது பிரச்சனை உள்ளது. இப்போது பொதுவான ஸ்கீமா தெரியவில்லை, மேலும், ஹைவ் அட்டவணையில் தவறான ஸ்கீமா உள்ளது, ஏனெனில் ஒவ்வொரு புதிய பகிர்வும் ஸ்கீமாவில் ஒரு சிதைவை அறிமுகப்படுத்தியிருக்கலாம்.

நீங்கள் அட்டவணையை மீண்டும் பதிவு செய்ய வேண்டும். இதை எளிமையாகச் செய்யலாம்: ஸ்டோர்ஃபிரண்டின் பார்க்வெட்டை மீண்டும் படித்து, ஸ்கீமாவை எடுத்து அதன் அடிப்படையில் ஒரு டிடிஎல்லை உருவாக்கவும், இதன் மூலம் ஹைவில் உள்ள கோப்புறையை வெளிப்புற அட்டவணையாக மீண்டும் பதிவுசெய்து, இலக்கு ஸ்டோர்ஃபிரண்டின் திட்டத்தைப் புதுப்பிக்கவும்.

எங்களுக்கு நான்காவது பிரச்சனை உள்ளது. நாங்கள் முதல் முறையாக அட்டவணையை பதிவு செய்தபோது, ​​​​நாங்கள் ஸ்பார்க்கை நம்பியிருந்தோம். இப்போது அதை நாமே செய்கிறோம், மேலும் ஹைவ்க்கு அனுமதிக்கப்படாத எழுத்துக்களுடன் பார்க்வெட் புலங்கள் தொடங்கலாம் என்பதை நினைவில் கொள்ள வேண்டும். எடுத்துக்காட்டாக, "corrupt_record" புலத்தில் அலச முடியாத வரிகளை Spark எறிகிறது. அத்தகைய புலத்தை தப்பிக்காமல் ஹைவில் பதிவு செய்ய முடியாது.

இதை அறிந்தால், நாங்கள் திட்டத்தைப் பெறுகிறோம்:

f_def = ""
for f in pf.dtypes:
  if f[0] != "date_load":
    f_def = f_def + "," + f[0].replace("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("array<`", "array<") 
table_define = "CREATE EXTERNAL TABLE jsonevolvtable (" + f_def[1:] + " ) "
table_define = table_define + "PARTITIONED BY (date_load string) STORED AS PARQUET LOCATION '/user/admin/testJson/testSchemaEvolution/pq/'"
hc.sql("drop table if exists jsonevolvtable")
hc.sql(table_define)

குறியீடு ("_corrupt_record", "`_corrupt_record`") + " " + f[1].replace(":", "`:").replace("<", "<`").replace(",", ",`").replace("array<`", "array<") பாதுகாப்பான DDL ஐ உருவாக்குகிறது, அதாவது இதற்கு பதிலாக:

create table tname (_field1 string, 1field string)

"_field1, 1field" போன்ற புலப் பெயர்களுடன், புலத்தின் பெயர்கள் தப்பிய இடத்தில் பாதுகாப்பான DDL ஆனது: அட்டவணை `tname` (`_field1` சரம், `1field` சரம்) உருவாக்கவும்.

கேள்வி எழுகிறது: ஒரு முழுமையான ஸ்கீமாவுடன் (pf குறியீட்டில்) டேட்டாஃப்ரேமை எவ்வாறு சரியாகப் பெறுவது? இந்த pf பெறுவது எப்படி? இது ஐந்தாவது பிரச்சனை. இலக்கு காட்சி பெட்டியின் பார்க்வெட் கோப்புகளுடன் கோப்புறையிலிருந்து அனைத்து பகிர்வுகளின் திட்டத்தை மீண்டும் படிக்கவா? இந்த முறை பாதுகாப்பானது, ஆனால் கடினமானது.

ஸ்கீமா ஏற்கனவே ஹைவில் உள்ளது. முழு அட்டவணையின் திட்டத்தையும் புதிய பகிர்வையும் இணைப்பதன் மூலம் நீங்கள் ஒரு புதிய திட்டத்தைப் பெறலாம். எனவே நீங்கள் ஹைவில் இருந்து டேபிள் ஸ்கீமாவை எடுத்து புதிய பகிர்வின் திட்டத்துடன் இணைக்க வேண்டும். ஹைவ் இலிருந்து சோதனை மெட்டாடேட்டாவைப் படித்து, அதை ஒரு தற்காலிக கோப்புறையில் சேமித்து, இரண்டு பகிர்வுகளையும் ஒரே நேரத்தில் படிக்க ஸ்பார்க்கைப் பயன்படுத்தி இதைச் செய்யலாம்.

உண்மையில், உங்களுக்கு தேவையான அனைத்தும் உள்ளன: ஹைவ் மற்றும் புதிய பகிர்வில் அசல் டேபிள் ஸ்கீமா. எங்களிடம் தரவுகளும் உள்ளன. உருவாக்கப்பட்ட பகிர்விலிருந்து ஸ்டோர்ஃபிரண்ட் ஸ்கீமா மற்றும் புதிய புலங்களை இணைக்கும் புதிய திட்டத்தைப் பெறுவதற்கு மட்டுமே இது உள்ளது:

from pyspark.sql import HiveContext
from pyspark.sql.functions import lit
hc = HiveContext(spark)
df = spark.read.json("...", dropFieldIfAllNull=True)
df.write.mode("overwrite").parquet(".../date_load=12-12-2019")
pe = hc.sql("select * from jsonevolvtable limit 1")
pe.write.mode("overwrite").parquet(".../fakePartiton/")
pf = spark.read.option("mergeSchema", True).parquet(".../date_load=12-12-2019/*", ".../fakePartiton/*")

அடுத்து, முந்தைய துணுக்கைப் போலவே அட்டவணைப் பதிவு DDL ஐ உருவாக்குகிறோம்.
முழு சங்கிலியும் சரியாக வேலை செய்தால், அதாவது துவக்க சுமை இருந்தது, மற்றும் ஹைவில் அட்டவணை சரியாக உருவாக்கப்பட்டது, பின்னர் புதுப்பிக்கப்பட்ட டேபிள் ஸ்கீமாவைப் பெறுகிறோம்.

கடைசி பிரச்சனை என்னவென்றால், ஹைவ் டேபிளில் ஒரு பகிர்வை மட்டும் சேர்க்க முடியாது, ஏனெனில் அது உடைந்து விடும். ஹைவ் அதன் பகிர்வு கட்டமைப்பை சரிசெய்ய நீங்கள் கட்டாயப்படுத்த வேண்டும்:

from pyspark.sql import HiveContext
hc = HiveContext(spark) 
hc.sql("MSCK REPAIR TABLE " + db + "." + destTable)

JSON ஐப் படித்து, அதன் அடிப்படையில் ஒரு கடை முகப்பை உருவாக்குவது, பல மறைமுகமான சிரமங்களைச் சமாளித்து, அதற்கான தீர்வுகளை நீங்கள் தனித்தனியாகத் தேட வேண்டும். இந்த தீர்வுகள் எளிமையானவை என்றாலும், அவற்றைக் கண்டுபிடிக்க நிறைய நேரம் எடுக்கும்.

காட்சி பெட்டியின் கட்டுமானத்தை செயல்படுத்த, நான் செய்ய வேண்டியிருந்தது:

  • ஷோகேஸில் பகிர்வுகளைச் சேர்க்கவும், சேவை கோப்புகளை அகற்றவும்
  • ஸ்பார்க் தட்டச்சு செய்த மூலத் தரவில் உள்ள காலியான புலங்களைக் கையாளவும்
  • எளிய வகைகளை ஒரு சரத்திற்கு அனுப்பவும்
  • புலப் பெயர்களை சிற்றெழுத்துக்கு மாற்றவும்
  • ஹைவில் தனி தரவு பதிவேற்றம் மற்றும் அட்டவணை பதிவு (DDL தலைமுறை)
  • ஹைவ் உடன் பொருந்தாத புலப் பெயர்களில் இருந்து தப்பிக்க மறக்காதீர்கள்
  • ஹைவில் அட்டவணைப் பதிவை எவ்வாறு புதுப்பிப்பது என்பதை அறிக

சுருக்கமாக, கடை ஜன்னல்களை உருவாக்குவதற்கான முடிவு பல ஆபத்துகளால் நிறைந்துள்ளது என்பதை நாங்கள் கவனிக்கிறோம். எனவே, செயல்படுத்துவதில் சிரமங்கள் ஏற்பட்டால், வெற்றிகரமான நிபுணத்துவத்துடன் அனுபவம் வாய்ந்த கூட்டாளரைத் தொடர்புகொள்வது நல்லது.

இந்த கட்டுரையைப் படித்ததற்கு நன்றி, தகவல் உங்களுக்கு பயனுள்ளதாக இருக்கும் என்று நம்புகிறோம்.

ஆதாரம்: www.habr.com

கருத்தைச் சேர்